MintUI 组件

导入

  1. 导入所有MintUI组件:

    1
    2
    3
    4
    5
    6
    // 导入所有的 MIntUI 组件
    import MintUI from "mint-ui"; //把所有的组件都导入进来
    // 这里 可以省略 node_modules 这一层目录
    import "mint-ui/lib/style.css";
    // 将 MintUI 安装到 Vue 中
    Vue.use(MintUI); // 把所有的组件,注册为全局的组件
  2. 按需导入

    1
    2
    3
    4
    5
    // 按需导入 Mint-UI组件
    import { Button } from "mint-ui";
    // 使用 Vue.component 注册 按钮组件
    Vue.component(Button.name, Button);
    // console.log(Button.name)
  3. 使用的例子:

    1
    <mt-button type="primary" size="large">primary</mt-button>

MUI 代码片段

  • 注意: MUI 不同于 Mint-UI,MUI只是开发出来的一套好用的代码片段,里面提供了配套的样式、配套的HTML代码段,类似于 Bootstrap;

  • 而 Mint-UI,是真正的组件库,是使用 Vue 技术封装出来的 成套的组件,可以无缝的和 VUE项目进行集成开发;

  • 从体验上来说, Mint-UI体验更好,因为这是别人帮我们开发好的现成的Vue组件; MUI和Bootstrap类似;

  • 理论上,任何项目都可以使用 MUI 或 Bootstrap,但是,MInt-UI只适用于Vue项目;

  • 注意: MUI 并不能使用 npm 去下载,需要自己手动从 github 上,下载现成的包,自己解压出来,然后手动拷贝到项目中使用;

  • 官网首页 文档地址

使用

  1. 导入 MUI 的样式表:

    1
    import "./lib/mui/css/mui.min.css";
  2. webpack.config.js中添加新的loader规则:

    1
    { test: /\.(png|jpg|gif|ttf)$/, use: 'url-loader' }
  3. 根据官方提供的文档和example,尝试使用相关的组件

    1
    2
    <button type="button" class="mui-btn ">默认</button>
    <div class="mui-btn mui-btn-primary mui-btn-outlined">蓝色</div>

布局示例

  1. 头部的固定导航栏使用 Mint-UIHeader 组件;

  2. 底部的页签使用 muitabbar;

  3. 购物车的图标,使用 icons-extra 中的 mui-icon-extra mui-icon-extra-cart,同时,应该把其依赖的字体图标文件 mui-icons-extra.ttf,复制到 fonts 目录下!

  4. 将底部的页签,改造成 router-link 来实现单页面的切换;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    <template>
    <div class="app-container">
    <!-- 顶部 Header 区域 -->
    <mt-header fixed title="黑马程序员·Vue项目"></mt-header>

    <!-- 中间的 路由 router-view 区域 -->
    <transition>
    <router-view></router-view>
    </transition>

    <!-- 底部 Tabbar 区域 -->
    <nav class="mui-bar mui-bar-tab">
    <router-link class="mui-tab-item" to="/home">
    <span class="mui-icon mui-icon-home"></span>
    <span class="mui-tab-label">首页</span>
    </router-link>
    <router-link class="mui-tab-item" to="/member">
    <span class="mui-icon mui-icon-contact"></span>
    <span class="mui-tab-label">会员</span>
    </router-link>
    <router-link class="mui-tab-item" to="/shopcar">
    <span class="mui-icon mui-icon-extra mui-icon-extra-cart">
    <span class="mui-badge">0</span>
    </span>
    <span class="mui-tab-label">购物车</span>
    </router-link>
    <router-link class="mui-tab-item" to="/search">
    <span class="mui-icon mui-icon-search"></span>
    <span class="mui-tab-label">搜索</span>
    </router-link>
    </nav>
    </div>
    </template>

    <script></script>

    <style lang="scss" scoped>
    .app-container {
    padding-top: 40px;
    overflow-x: hidden;
    }

    .v-enter {
    opacity: 0;
    transform: translateX(100%);
    }

    .v-leave-to {
    opacity: 0;
    transform: translateX(-100%);
    position: absolute;
    }

    .v-enter-active,
    .v-leave-active {
    transition: all 0.5s ease;
    }
    </style>

高亮路由

  1. 全局设置样式如下:

    1
    2
    3
    .router-link-active {
    color: #007aff !important;
    }
  2. new VueRouter 的时候,通过 linkActiveClass 来指定高亮的类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 创建路由对象
    var router = new VueRouter({
    routes: [
    { path: "/", redirect: "/home" },
    { path: "/home", component: Home },
    { path: "/member", component: Member },
    { path: "/shopcar", component: Shopcar },
    { path: "/search", component: Search },
    ],
    // 覆盖默认的路由高亮的类,默认的类叫做 router-link-active
    linkActiveClass: "mui-active",
    });

兼容问题

  1. App.vue 中的 router-link 身上的类名 mui-tab-item 存在兼容性问题,导致tab栏失效,可以把mui-tab-item改名为mui-tab-item1,并复制相关的类样式,来解决这个问题;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.mui-bar-tab .mui-tab-item1 .mui-active {
color: #007aff;
}

.mui-bar-tab .mui-tab-item1 {
display: table-cell;
overflow: hidden;
width: 1%;
height: 50px;
text-align: center;
vertical-align: middle;
white-space: nowrap;
text-overflow: ellipsis;
color: #929292;
}

.mui-bar-tab .mui-tab-item1 .mui-icon {
top: 3px;
width: 24px;
height: 24px;
padding-top: 0;
padding-bottom: 0;
}

.mui-bar-tab .mui-tab-item1 .mui-icon ~ .mui-tab-label {
font-size: 11px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}

文件读取

  • 封装一个方法,调用者提供要读取文件的路径,编写方法能读取文件,并把内容返回

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const fs = require("fs");
    const path = require("path");

    // 这是普通读取文件的方式
    fs.readFile(
    path.join(__dirname, "./files/1.txt"),
    "utf-8",
    (err, dataStr) => {
    if (err) return console.log(err.message);
    console.log(dataStr);
    },
    );
  • 封装成方法,添加一个回调callback ,规定 callback 中有两个参数,第一个参数,是失败的结果;第二个参数是成功的结果;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 我们可以
    function getFileByPath(fpath, callback) {
    fs.readFile(fpath, 'utf-8', (err, dataStr) => {
    // 失败了,则第一个位置放 Error对象,第二个位置防止一个 undefined
    if (err) return callback(err)
    // 成功返回的结果,应该位于 callback 参数的第二个位置,
    //第一个位置 由于没有出错,所以,放一个 null;
    callback(null, dataStr)
    })
    }

    // 调用
    var callback = (
    function (data) {
    console.log(data + '娃哈哈,成功了!!!')
    }, function (err) {
    console.log('失败的结果,我们使用失败的回调处理了一下:' + err.message)
    })

    var result = getFileByPath(path.join(__dirname, './1.txt'),callback)
  • 使用Promise改写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function getFileByPath(fpath) {
    return new Promise(function (resolve, reject) {
    fs.readFile(fpath, "utf-8", (err, dataStr) => {
    if (err) return reject(err);
    resolve(dataStr);
    });
    });
    }

    getFileByPath("./files/2.txt").then(
    function (data) {
    console.log(data + "-------");
    },
    function (err) {
    console.log(err.message);
    },
    );

eslint

  • 用来做项目编码规范检查的工具
  • 基本原理: 定义了很多规则, 检查项目的代码一旦发现违背了某个规则就输出相应的提示信息
  • 有相应的配置, 可定制检查

Mixin

  • Mixin是一种更好的复用代码的模式.我们知道 java , Object C 中的 interface , implements, extends 等关键字的意义,就 是为了让代码可以复用、继承.但是这几种方法, 都理解起来很不直观, 给人一种拐弯抹角的感觉. 特别是像我这样很不习惯 “设计 模式”的人。
  • 在js, ruby等动态语言中, 我们如果要复用代码的话,直接使用 mixin 就好了.
  • Mixin 的 概念Mixin 实际上是利用语言的特性(关键字),以更加简洁易懂的方式,实现了 “设计模式”中的 “组合模式”。 可以定义一个公共的类,这个类就叫做”mixin”.然后让其他的类,通过“include” 这样的语言特性,来包含mixin, 直接具备了 mixin 所具备的各种方法。

示例

  • 建立一个Mixin文件,可以在 src/mixin 目录下创建, 例如: 文件: src/mixin/common_hi.js :

    1
    2
    3
    4
    5
    6
    7
    export default {
    methods: {
    hi: function (name) {
    return "你好, " + name;
    },
    },
    };
  • 使用

    • Mixin使用起来很简单,在对应的 js文件, 或者 vue文件的<script>代码中引用即可.

    • 例如,新建一个vue文件:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <template>
      <div>
      {{ hi("from view") }}
      </div>
      </template>

      <script>
      import CommonHi from "@/mixins/common_hi.js";
      export default {
      mixins: [CommonHi],
      mounted() {
      alert(this.hi("from script code"));
      },
      };
      </script>
    • 注意:

      • 使用的时候, mixins: [CommonHi] 这里的是中括号,表示是数组.
      • js代码中调用的话, 需要带有this关键字,例如: this.hi()
    • 配置vue页面的路由如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import SayHiFromMixin from "@/components/SayHiFromMixin";
      export default new Router({
      routes: [
      {
      path: "/say_hi_from_mixin",
      name: "SayHiFromMixin",
      component: SayHiFromMixin,
      },
      ],
      });
    • 查看运行效果 http://localhost:8080/#/say_hi_from_mixin

vuex

概念

  • vuex 是 Vue 配套的 公共数据管理工具,它可以把一些共享的数据,保存到 vuex 中,方便 整个程序中的任何组件直接获取或修改我们的公共数据

状态自管理应用

  • state: 驱动应用的数据源

  • view: 以声明方式将state映射到视图

  • actions: 响应在view上的用户输入导致的状态变化(包含n个更新状态的方法)

多组件共享状态的问题

  • 多个视图依赖于同一状态
  • 来自不同视图的行为需要变更同一状态
  • 以前的解决办法
    • 将数据以及操作数据的行为都定义在父组件
    • 将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递)
  • vuex就是用来解决多组件共享状态问题

vuex的核心概念

state

vuex管理的状态对象
它应该是唯一的
const state = {
    xxx: initValue
}

mutations

包含多个直接更新state的方法(回调函数)的对象
谁来触发: action中的commit('mutation名称')
只能包含同步的代码, 不能写异步代码
const mutations = {
    yyy (state, data) {
        // 更新state的某个属性
    }
}

actions

包含多个事件回调函数的对象
通过执行: commit()来触发mutation的调用, 间接更新state
谁来触发: 组件中: $store.dispatch('action名称')  // 'zzz'
可以包含异步代码(定时器, ajax)
const actions = {
    zzz ({commit, state}, data1) {
        commit('yyy', data2)
    }
}

getters

包含多个计算属性(get)的对象
谁来读取: 组件中: $store.getters.xxx
const getters = {
    mmm (state) {
        return ...
    }
}

modules

包含多个module
一个module是一个store的配置对象
与一个组件(包含有共享数据)对应

向外暴露store对象

export default new Vuex.Store({
    state,
    mutations,
    actions,
    getters
})
  • 组件中:

    1
    2
    3
    4
    5
    6
    7
    import {mapGetters, mapActions} from 'vuex'
    export default {
    computed: mapGetters(['mmm'])
    methods: mapActions(['zzz'])
    }

    {{mmm}} @click="zzz(data)"

映射store

import store from './store'
new Vue({
    store
})

store对象

1.所有用vuex管理的组件中都多了一个属性$store, 它就是一个store对象
2.属性:
  state: 注册的state对象
  getters: 注册的getters对象
3.方法:
  dispatch(actionName, data): 分发action

配置vuex

  1. 运行 cnpm i vuex -S

  2. main.js导入包 import Vuex from 'vuex'

  3. 注册vuex到vue中 Vue.use(Vuex)

  4. new Vuex.Store() 实例,得到一个数据仓储对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    var store = new Vuex.Store({
    state: {
    // 大家可以把 state 想象成 组件中的 data ,专门用来存储数据的
    // 如果在 组件中,想要访问,store 中的数据,只能通过 this.$store.state. 来访问
    count: 0,
    },
    mutations: {
    // 注意: 如果要操作 store 中的 state 值,只能通过调用 mutations 提供的方法,才能操作对应的数据
    // 不推荐直接操作 state 中的数据,因为万一导致了数据的紊乱,不能快速定位到错误的原因,因为,每个组件都可能有操作数据的方法;
    increment(state) {
    state.count++;
    },
    // 注意: 如果组件想要调用 mutations 中的方法,只能使用 this.$store.commit('方法名')
    // 这种 调用 mutations 方法的格式,和 this.$emit('父组件中方法名')
    subtract(state, obj) {
    // 注意: mutations 的 函数参数列表中,最多支持两个参数,
    // 其中,参数1: 是 state 状态; 参数2: 通过 commit 提交过来的参数;
    console.log(obj);
    state.count -= obj.c + obj.d;
    },
    },
    getters: {
    // 注意:这里的 getters, 只负责对外提供数据,不负责修改数据,如果想要修改 state 中的数据,请去找 mutations
    optCount: function (state) {
    return "当前最新的count值是:" + state.count;
    },
    // 经过咱们回顾对比,发现 getters 中的方法, 和组件中的过滤器比较类似,因为 过滤器和 getters 都没有修改原数据, 都是把原数据做了一层包装,提供给了 调用者;
    // 其次, getters 也和 computed 比较像, 只要 state 中的数据发生变化了,那么,如果 getters 正好也引用了这个数据,那么 就会立即触发 getters 的重新求值;
    },
    });
  5. 使用示例

    • vuex
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    var store = new Vuex.Store({
    state: {
    // this.$store.state.***
    car: car, // 将 购物车中的商品的数据,用一个数组存储起来,在 car 数组中,存储一些商品的对象, 咱们可以暂时将这个商品对象,设计成这个样子
    // { id:商品的id, count: 要购买的数量, price: 商品的单价,selected: false }
    },
    mutations: {
    // this.$store.commit('方法的名称', '按需传递唯一的参数')
    addToCar(state, goodsinfo) {
    // 点击加入购物车,把商品信息,保存到 store 中的 car 上
    // 分析:
    // 1. 如果购物车中,之前就已经有这个对应的商品了,那么,只需要更新数量
    // 2. 如果没有,则直接把 商品数据,push 到 car 中即可

    // 假设 在购物车中,没有找到对应的商品
    var flag = false;

    state.car.some((item) => {
    if (item.id == goodsinfo.id) {
    item.count += parseInt(goodsinfo.count);
    flag = true;
    return true;
    }
    });

    // 如果最终,循环完毕,得到的 flag 还是 false,则把商品数据直接 push 到 购物车中
    if (!flag) {
    state.car.push(goodsinfo);
    }

    // 当 更新 car 之后,把 car 数组,存储到 本地的 localStorage 中
    localStorage.setItem("car", JSON.stringify(state.car));
    },
    updateGoodsInfo(state, goodsinfo) {
    // 修改购物车中商品的数量值
    // 分析:
    state.car.some((item) => {
    if (item.id == goodsinfo.id) {
    item.count = parseInt(goodsinfo.count);
    return true;
    }
    });
    // 当修改完商品的数量,把最新的购物车数据,保存到 本地存储中
    localStorage.setItem("car", JSON.stringify(state.car));
    },
    removeFormCar(state, id) {
    // 根据Id,从store 中的购物车中删除对应的那条商品数据
    state.car.some((item, i) => {
    if (item.id == id) {
    state.car.splice(i, 1);
    return true;
    }
    });
    // 将删除完毕后的,最新的购物车数据,同步到 本地存储中
    localStorage.setItem("car", JSON.stringify(state.car));
    },
    updateGoodsSelected(state, info) {
    state.car.some((item) => {
    if (item.id == info.id) {
    item.selected = info.selected;
    }
    });
    // 把最新的 所有购物车商品的状态保存到 store 中去
    localStorage.setItem("car", JSON.stringify(state.car));
    },
    },
    getters: {
    // this.$store.getters.***
    // 相当于 计算属性,也相当于 filters
    getAllCount(state) {
    var c = 0;
    state.car.forEach((item) => {
    c += item.count;
    });
    return c;
    },
    getGoodsCount(state) {
    var o = {};
    state.car.forEach((item) => {
    o[item.id] = item.count;
    });
    return o;
    },
    getGoodsSelected(state) {
    var o = {};
    state.car.forEach((item) => {
    o[item.id] = item.selected;
    });
    return o;
    },
    getGoodsCountAndAmount(state) {
    var o = {
    count: 0, // 勾选的数量
    amount: 0, // 勾选的总价
    };
    state.car.forEach((item) => {
    if (item.selected) {
    o.count += item.count;
    o.amount += item.price * item.count;
    }
    });
    return o;
    },
    },
    });
    • 使用

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      methods: {
      getGoodsList() {
      // 1. 获取到 store 中所有的商品的Id,然后拼接出一个 用逗号分隔的 字符串
      var idArr = [];
      this.$store.state.car.forEach(item => idArr.push(item.id));
      // 如果 购物车中没有商品,则直接返回,不需要请求数据接口,否则会报错
      if (idArr.length <= 0) {
      return;
      }
      // 获取购物车商品列表
      this.$http
      .get("api/goods/getshopcarlist/" + idArr.join(","))
      .then(result => {
      if (result.body.status === 0) {
      this.goodslist = result.body.message;
      }
      });
      },
      remove(id, index) {
      // 点击删除,把商品从 store 中根据 传递的 Id 删除,同时,把 当前组件中的 goodslist 中,对应要删除的那个商品,使用 index 来删除
      this.goodslist.splice(index, 1);
      this.$store.commit("removeFormCar", id);
      },
      selectedChanged(id, val) {
      // 每当点击开关,把最新的 快关状态,同步到 store 中
      // console.log(id + " --- " + val);
      this.$store.commit("updateGoodsSelected", { id, selected: val });
      }
      }

总结:

  1. state中的数据,不能直接修改,如果想要修改,必须通过 mutations

  2. 如果组件想要直接 从 state 上获取数据: 需要 this.$store.state.属性名

  3. 如果 组件,想要修改数据,必须使用 mutations 提供的方法,需要通过 this.$store.commit('方法的名称', 唯一的一个参数)

  4. 如果 store 中 state 上的数据, 在对外提供的时候,需要做一层包装,那么 ,推荐使用 getters, 如果需要使用 getters ,则用 this.$store.getters.

vuex 模块化

  • 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

  • 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)

    • 每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const moduleA = {
    state: () => ({ ... }),
    mutations: { ... },
    actions: { ... },
    getters: { ... }
    }

    const moduleB = {
    state: () => ({ ... }),
    mutations: { ... },
    actions: { ... }
    }

    const store = new Vuex.Store({
    modules: {
    a: moduleA,
    b: moduleB
    }
    })

    store.state.a // -> moduleA 的状态
    store.state.b // -> moduleB 的状态

模块的局部状态

  • 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const moduleA = {
    state: () => ({
    count: 0,
    }),
    mutations: {
    increment(state) {
    // 这里的 `state` 对象是模块的局部状态
    state.count++;
    },
    },

    getters: {
    doubleCount(state) {
    return state.count * 2;
    },
    },
    };
  • 同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const moduleA = {
    // ...
    actions: {
    incrementIfOddOnRootSum({ state, commit, rootState }) {
    if ((state.count + rootState.count) % 2 === 1) {
    commit("increment");
    }
    },
    },
    };
  • 对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

    1
    2
    3
    4
    5
    6
    7
    8
    const moduleA = {
    // ...
    getters: {
    sumWithRootCount(state, getters, rootState) {
    return state.count + rootState.count;
    },
    },
    };

#命名空间

  • 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

  • 如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    const store = new Vuex.Store({
    modules: {
    account: {
    namespaced: true,

    // 模块内容(module assets)
    state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
    getters: {
    isAdmin () { ... } // -> getters['account/isAdmin']
    },
    actions: {
    login () { ... } // -> dispatch('account/login')
    },
    mutations: {
    login () { ... } // -> commit('account/login')
    },

    // 嵌套模块
    modules: {
    // 继承父模块的命名空间
    myPage: {
    state: () => ({ ... }),
    getters: {
    profile () { ... } // -> getters['account/profile']
    }
    },

    // 进一步嵌套命名空间
    posts: {
    namespaced: true,

    state: () => ({ ... }),
    getters: {
    popular () { ... } // -> getters['account/posts/popular']
    }
    }
    }
    }
    }
    })
  • 启用了命名空间的 getter 和 action 会收到局部化的 getterdispatchcommit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

#在带命名空间的模块内访问全局内容(Global Assets)

如果你希望使用全局 state 和 getter,rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
modules: {
foo: {
namespaced: true,

getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},

actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'

dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}

#在带命名空间的模块注册全局 action

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,

actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}

#带命名空间的绑定函数

当使用 mapState, mapGetters, mapActionsmapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}

而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createNamespacedHelpers } from "vuex";

const { mapState, mapActions } = createNamespacedHelpers("some/nested/module");

export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: (state) => state.a,
b: (state) => state.b,
}),
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions(["foo", "bar"]),
},
};

#给插件开发者的注意事项

如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:

1
2
3
4
5
6
7
8
9
// 通过插件的参数对象得到空间名称
// 然后返回 Vuex 插件函数
export function createPlugin(options = {}) {
return function (store) {
// 把空间名字添加到插件模块的类型(type)中去
const namespace = options.namespace || "";
store.dispatch(namespace + "pluginAction");
};
}

#模块动态注册

在 store 创建之后,你可以使用 store.registerModule 方法注册模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vuex from "vuex";

const store = new Vuex.Store({
/* 选项 */
});

// 注册模块 `myModule`
store.registerModule("myModule", {
// ...
});
// 注册嵌套模块 `nested/myModule`
store.registerModule(["nested", "myModule"], {
// ...
});

之后就可以通过 store.state.myModulestore.state.nested.myModule 访问模块的状态。

模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync (opens new window)插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。

你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。

注意,你可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到 store。

#保留 state

在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState 选项将其归档:store.registerModule('a', module, { preserveState: true })

当你设置 preserveState: true 时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。

#模块重用

有时我们可能需要创建一个模块的多个实例,例如:

如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。

实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):

1
2
3
4
5
6
const MyReusableModule = {
state: () => ({
foo: "bar",
}),
// mutation, action 和 getter 等等...
};

vue-router

  • vue用来实现SPA的插件

使用vue-router

  1. 创建路由器: router/index.js

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    new VueRouter({
    routes: [
    {
    // 一般路由
    path: "/about",
    component: about,
    },
    {
    // 自动跳转路由
    path: "/",
    redirect: "/about",
    },
    ],
    });
  2. 注册路由器: main.js

    1
    2
    3
    4
    import router from "./router";
    new Vue({
    router,
    });
  3. 使用路由组件标签:

    1
    2
    <router-link to="/xxx">Go to XXX</router-link>
    <router-view></router-view>

编写路由的3步

  1. 定义路由组件

  2. 映射路由

  3. 编写路由2个标签

  4. 嵌套路由

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    children: [
    {
    path: "/home/news",
    component: news,
    },
    {
    path: "message",
    component: message,
    },
    ];
  5. 向路由组件传递数据

    1
    2
    params: <router-link to="/home/news/abc/123">
    props: <router-view msg='abc'>
  6. 缓存路由组件

    1
    2
    3
    <keep-alive>
    <router-view></router-view>
    </keep-alive>
  7. 路由的编程式导航

    1
    2
    3
    this.$router.push(path): 相当于点击路由链接(可以返回到当前路由界面)
    this.$router.replace(path): 用新路由替换当前路由(不可以返回到当前路由界面)
    this.$router.back(): 请求(返回)上一个记录路由

ajax

  • 相关库:
    • vue-resource: vue插件, 多用于vue1.x,官方文档
    • axios: 第三方库, 多用于vue2.x

vue-resource使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 引入模块
import VueResource from "vue-resource";
// 使用插件
Vue.use(VueResource);

// 通过vue/组件对象发送ajax请求
this.$http.get("/someUrl").then(
(response) => {
// success callback
console.log(response.data); //返回结果数据
},
(response) => {
// error callback
console.log(response.statusText); //错误信息
},
);

axios使用

1
2
3
4
5
6
7
8
9
10
11
12
// 引入模块
import axios from "axios";

// 发送ajax请求
axios
.get(url)
.then((response) => {
console.log(response.data); // 得到返回结果数据
})
.catch((error) => {
console.log(error.message);
});

设置 vue开发服务器的代理

  • 正常来说, javascript在浏览器中是无法发送跨域请求的,所以我们需要在vuejs的"开发服务器"上做 个转发配置.

  • 修改: config/index.js文件,增加下列内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    module.exports = {
    dev: {
    proxyTable: {
    '/api': { // 1. 对于所有以 "/api" 开头的url 做处理.
    target: 'http://test.com', // 3. 转发到 siwei.me 上.
    changeOrigin: true,
    pathRewrite: {
    '^/api': '' // 2. 把url中的 "/api" 去掉.
    }
    }
    },
    }
  • 上面的代码做了三件事:

    • 对于所有以 “/api” 开头的url 做处理.
    • 把url中的 “/api” 去掉.
    • 把新的url 请求打到 siwei.me 上.
  • 例如:

  • 注意:以上的代理服务器内容,只能在"开发模式"下才能使用.在生产模式下,只能靠服务器的nginx的 特性来解决js跨域问题.

  • 重启服务器,可以看到我们的转发设置已经生效:

    1
    2
    3
    4
    5
    6
    $ npm run dev
    ...
    [HPM] Proxy created: /api -> http://siwei.me
    [HPM] Proxy rewrite rule created: "^/api" ~> ""
    > Starting dev server...
    ...

打包部署

  • 直接使用下面命令,就可以把vue项目打包:

    s
    1
    $ npm run build
  • 将打包的dist目录上传到服务器

    1
    2
    3
    4
    # 把本地的 /dist 目录,上传到了远程的 /opt/app目录上
    scp -P 6666 -r dist root@192.168.10.10:/opt/app
    # 上传的文件夹重命名成: vue_demo
    mv /opt/app/dist /opt/app/vue_demo
  • 配置nginx, 使域名: vue_demo.test.com 指向该位置:

    • 把下面代码,加入到nginx的配置文件中(/etc/nginx/nginx.conf)

      1
      2
      3
      4
      5
      6
      7
      server {
      listen 80;
      server_name vue_demo.test.com;
      client_max_body_size 500m;
      charset utf-8;
      root /opt/app/vue_demo;
      }
    • 启动nginx

      1
      2
      3
      # nginx -t
      # nginx -s stop
      # nginx
    • nginx跑起来之后, 我们就要配置域名. 否则无法访问.

      • 新增加二级域名: vue_demo.test.com,以dnspod为例, 需要设置这个二级域名的: A记录. IP地址(部署nginx的机器):
      • 配制完成后,回到命令行, 输入 ping vue_demo.test.com命令,确认后返回的ip正确表示配制完成,可以访问。

解决域名问题与跨域问题

  • 在部署之后, 会发现Vuejs会遇到js 的经典问题: 远程服务器地址不对,或者跨域问题.

  • 前提:我们的真正后台接口是: http://siwei.me/interface/blogs/all 如下:

域名404 问题

  • 这个问题看起来如下:

  • 这个问题是由于源代码中,访问 /interface/blogs/all 这个接口引起的:

    1
    this.$http.get('/api/interface/blogs/all')...
  • 在我们开发的时候, vuejs 会通过 $npm run dev 命令, 跑起一个 “开发服务器”, 这个server中有一个代理, 可以把所有的 以 ‘/api’ 开头的请求,例如:

    1
    2
    3
    localhost: 8080 / api / interface / blogs / all;
    // 转发到:
    siwei.me / interface / blogs / all;
  • “开发服务器”的配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    proxyTable: {
    '/api': {
    target: 'http://siwei.me',
    changeOrigin: true,
    pathRewrite: {
    '^/api': ''
    }
    }
    },
  • 所以, 在开发环境下,一切正常.但是在生产环境中, 发起请求的时候, 就不存在代理服务器,不存在dev server了,所以会出错.

  • 解决方式有两种,一是使用代理,如何解决接口路径请求404的问题,二是直接写请求路径,需要解决cookie丢失的问题

跨域问题

  • 这个问题,是js的经典问题.如果vue_demo.siwei.me 直接访问siwei.me域名下的资源,会报错. 因为他们是两个不同的域名.

  • 代码形如:

    1
    this.$http.get('http://siwei.me/api/interface/blogs/all')...
  • 我们就会发现:

    1
    2
    3
    XMLHttpRequest cannot load http://siwei.me/api/interface/blogs/all.
    No 'Access-Control-Allow-Origin' header is present on the requested resource.
    Origin 'http://vue_demo.siwei.me' is therefore not allowed access.
  • 完整过程如下:

解决域名和跨域问题

  1. 在代码端, 处理方式不变, 访问 /api + 原接口url:

    1
    this.$http.get('/api/interface/blogs/all')...
  2. 在开发的时候, 继续保持vuejs 的代理存在. 配置代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    proxyTable: {
    '/api': {
    target: 'http://siwei.me',
    changeOrigin: true,
    pathRewrite: {
    '^/api': ''
    }
    }
    },
  3. 在nginx的配置文件中,加入代理:(详细说明见代码中的注释)就可以了.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    server {
    listen 80;
    server_name vue_demo.siwei.me;
    client_max_body_size 500m;
    charset utf-8;
    root /opt/app/vue_demo;

    # 第一步,把所有的 mysite.com/api/interface 转换成: mysite.com/interface
    location /api {
    rewrite ^(.*)\/api(.*)$ $1$2;
    }

    # 第二步, 把所有的 mysite.com/interface 的请求,转发到 siwei.me/interface
    location /interface {
    proxy_pass http://siwei.me;
    }
    }
  4. 也就是说,上面的配置,把http://vue_demo.siwei.me/api/interface/blogs/all在服务器端做了个变换,相当于访问了:http://siwei.me/interface/blogs/all ,重启nginx ,就会发现生效了.如下所示:

原理理解

准备

1.[].slice.call(lis): 将伪数组转换为真数组
2.node.nodeType: 得到节点类型
3.Object.defineProperty(obj, propertyName, {}): 给对象添加/修改属性(指定描述符)
    configurable: true/false  是否可以重新define
    enumerable: true/false 是否可以枚举(for..in / keys())
    value: 指定初始值
    writable: true/false value是否可以修改存取(访问)描述符
    get: 函数, 用来得到当前属性值
    set: 函数, 用来监视当前属性值的变化
4.Object.keys(obj): 得到对象自身可枚举的属性名的数组
5.DocumentFragment: 文档碎片(高效批量更新多个节点)
6.obj.hasOwnProperty(prop): 判断prop是否是obj自身的属性

数据代理(MVVM.js)

1.通过一个对象代理对另一个对象中属性的操作(读/写)
2.通过vm对象来代理data对象中所有属性的操作
3.好处: 更方便的操作data中的数据
4.基本实现流程
    1). 通过Object.defineProperty()给vm添加与data对象的属性对应的属性描述符
    2). 所有添加的属性都包含getter/setter
    3). 在getter/setter内部去操作data中对应的属性数据

模板解析(compile.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
1.模板解析的关键对象: compile对象
2.模板解析的基本流程:
1). 将el的所有子节点取出, 添加到一个新建的文档fragment对象中
2). 对fragment中的所有层次子节点递归进行编译解析处理
* 对表达式文本节点进行解析
* 对元素节点的指令属性进行解析
* 事件指令解析
* 一般指令解析
3). 将解析后的fragment添加到el中显示
3.解析表达式文本节点: textNode.textContent = value
1). 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1
2). 从data中取出表达式对应的属性值
3). 将属性值设置为文本节点的textContent
4.事件指令解析: elementNode.addEventListener(事件名, 回调函数.bind(vm))
v-on:click="test"
1). 从指令名中取出事件名
2). 根据指令的值(表达式)从methods中得到对应的事件处理函数对象
3). 给当前元素节点绑定指定事件名和回调函数的dom事件监听
4). 指令解析完后, 移除此指令属性
5.一般指令解析: elementNode.xxx = value
1). 得到指令名和指令值(表达式)
2). 从data中根据表达式得到对应的值
3). 根据指令名确定需要操作元素节点的什么属性
* v-text---textContent属性
* v-html---innerHTML属性
* v-class--className属性
4). 将得到的表达式的值设置到对应的属性上
5). 移除元素的指令属性

数据劫持–>数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
1.数据绑定(model==>View):
1). 一旦更新了data中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会更新(更新)
2.数据劫持
1). 数据劫持是vue中用来实现数据绑定的一种技术
2). 基本思想: 通过defineProperty()来监视data中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
3.四个重要对象
1). Observer
* 用来对data所有属性数据进行劫持的构造函数
* 给data中所有属性重新定义属性描述(get/set)
* 为data中的每个属性创建对应的dep对象
2). Dep(Depend)
* data中的每个属性(所有层次)都对应一个dep对象
* 创建的时机:
* 在初始化define data中各个属性时创建对应的dep对象
* 在data中的某个属性值被设置为新的对象时
* 对象的结构
{
id, // 每个dep都有一个唯一的id
subs //包含n个对应watcher的数组(subscribes的简写)
}
* subs属性说明
* 当一个watcher被创建时, 内部会将当前watcher对象添加到对应的dep对象的subs中
* 当此data属性的值发生改变时, 所有subs中的watcher都会收到更新的通知, 从而最终更新对应的界面
3). Compile
* 用来解析模板页面的对象的构造函数(一个实例)
* 利用compile对象解析模板页面
* 每解析一个表达式(非事件指令)都会创建一个对应的watcher对象, 并建立watcher与dep的关系
* complie与watcher关系: 一对多的关系
4). Watcher
* 模板中每个非事件指令或表达式都对应一个watcher对象
* 监视当前表达式数据的变化
* 创建的时机: 在初始化编译模板时
* 对象的组成
{
vm, //vm对象
exp, //对应指令的表达式
cb, //当表达式所对应的数据发生改变的回调函数
value, //表达式当前的值
depIds //表达式中各级属性所对应的dep对象的集合对象
//属性名为dep的id, 属性值为dep
}

5). 总结: dep与watcher的关系: 多对多
* 一个data中的属性对应对应一个dep, 一个dep中可能包含多个watcher(模板中有几个表达式使用到了属性)
* 模板中一个非事件表达式对应一个watcher, 一个watcher中可能包含多个dep(表达式中包含了几个data属性)
* 数据绑定使用到2个核心技术
* defineProperty()
* 消息订阅与发布

4.双向数据绑定
1). 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
2). 双向数据绑定的实现流程:
* 在解析v-model指令时, 给当前元素添加input监听
* 当input的value发生改变时, 将最新的值赋值给当前表达式所对应的data属性