准备

首先创建项目

vue create sportsstore --default

安装jquery,bootstrap,popper.js

cd sportsstore
npm install jquery
npm install [email protected]
npm install popper

在 main.js文件中引入jquery和bootstrap

import Vue from 'vue'
import App from './App.vue'

// 引入jquery
import $ from 'jquery'

Vue.config.productionTip = false

// 添加bootstrap框架
import "bootstrap/dist/css/bootstrap.min.css"

new Vue({
  render: h => h(App),
}).$mount('#app')

配置package.json的eslintConfig里的rules

"rules": {
    "no-unused-vars":"off",
	"no-console":"off",
	"no-declare": "off"
},

运行项目

npm run serve

添加附加包

cd sportsstore
npm install [email protected]
npm install [email protected]
npm install [email protected]
npm install [email protected]
npm install [email protected]
npm install --save-dev [email protected]
npm install --save-dev [email protected]
npm install --save-dev [email protected]
  • axios 提供http请求服务接口
  • vue-router 提供浏览器端的路由服务
  • vuex 提供应用内共享数据存储管理
  • veulidate 提供用户输入数据验证服务
  • bootstrap 提供css样式
  • font-awesome 提供字体图标
  • json-server 提供restful风格的web服务
  • jsonwebtoken 用于生成认证token以获取授权
  • faker 用于生产测试数据

添加样式到项目

在main.js中添加如下代码

...
Vue.Config.productionTip = false
import "bootstrap/dist/css/bootstrap.min.css";
import "font-awesome/css/font-awesome.min.css";
...

准备restful web服务

在sportsstore/data.js下添加如下代码:

var data = [
  {
    id: 1,
    name: "Kayak",
    category: "Watersports",
    description: "A boat for one person",
    price: 275,
  },
  {
    id: 2,
    name: "Lifejacket",
    category: "Watersports",
    description: "Protective and fashionable",
    price: 48.95,
  },
  {
    id: 3,
    name: "Soccer Ball",
    category: "Soccer",
    description: "FIFA-approved size and weight",
    price: 19.5,
  },
  {
    id: 4,
    name: "Corner Flags",
    category: "Soccer",
    description: "Give your playing field a professional touch",
    price: 34.95,
  },
  {
    id: 5,
    name: "Stadium",
    category: "Soccer",
    description: "Flat-packed 35,000-seat stadium",
    price: 79500,
  },
  {
    id: 6,
    name: "Thinking Cap",
    category: "Chess",
    description: "Improve brain efficiency by 75%",
    price: 16,
  },
  {
    id: 7,
    name: "Unsteady Chair",
    category: "Chess",
    description: "Secretly give your opponent a disadvantage",
    price: 29.95,
  },
  {
    id: 8,
    name: "Human Chess Board",
    category: "Chess",
    description: "A func game for the family",
    price: 75,
  },
  {
    id: 9,
    name: "Bling Bling King",
    category: "Chess",
    description: "Gold-plated, diamond-studded King",
    price: 1200,
  },
];

module.exports = function() {
  return {
    products: data,
    categories: [...new Set(data.map((p) => p.category))].sort(),
    orders: [],
  };
};

在sportsstore/authMiddleware.js文件里添加如下代码:

const jwt = require("jsonwebtoken");

const APP_SECRET = "myappsecret";
const USERNAME = "admin";
const PASSWORD = "secret";

module.exports = function(req, res, next) {
  if (
    (req.url == "/api/login" || req.url == "/login") &&
    req.method == "POST"
  ) {
    if (
      req.body != null &&
      req.body.name == USERNAME &&
      req.body.password == PASSWORD
    ) {
      let token = jwt.sign({ data: USERNAME, expiresIn: "1h" }, APP_SECRET);
      res.json({ success: true, token: token });
    } else {
      res.json({ success: false });
    }
    res.end();
    return;
  } else if (
    ((req.url.startsWith("/api/products") ||
      req.url.startsWith("/products") ||
      req.url.startsWith("/api/categories") ||
        req.url.startsWith("/categories")) &&
      req.method != "GET") ||
    ((req.url.startsWith("/api/orders") || req.url.startsWith("/orders")) &&
      req.method != "POST")
  ) {
    let token = req.headers["authorization"];
    if (token != null && token.startsWith("Bearer<")) {
      token = token.substring(7, token.length - 1);
      try {
        jwt.verify(token, APP_SECRET);
        next();
        return;
      } catch (err) {}
    }
    res.statusCode = 401;
    res.end();
    return;
  }
  next();
};

在package.json中添加如下内容

"scripts": {
    "json": "json-server data.js -p 3500 -m authMiddleware.js"
}

运行web 服务

npm run json

在第二个终端中执行一下命令

npm run serve

创建数据存储

在src/store文件夹下添加index.js文件,内容如下:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const testData = [];

for(let i = 1; i <= 10; i++){
    testData.push({
        id: i, name: `Product #${i}`, category: `Category ${i % 3}`,
        description: `This is Product #${i}`, price: i * 50
    })
}

export default new Vuex.Store({
    strict: true,
    state: {
        products: testData
    }
})

添加vuex数据存储,在src/main.js中添加内容

import Vue from 'vue'
import App from './App.vue'

// 引入jquery
import $ from 'jquery'

Vue.config.productionTip = false

// 添加bootstrap框架
import "bootstrap/dist/css/bootstrap.min.css";
import "font-awesome/css/font-awesome.min.css";

// 添加vuex存储
import store from "./store";

new Vue({
  render: h => h(App),
  store // 挂在岛vue中
}).$mount('#app')

创建项目存储

在src/components文件夹下新建Store.vue文件,其内容如下:

<template>
<div class="container-fluid">
    <div class="row">
        <div class="col bg-dark text-white">
            <a class="navbar-brand">SPORTS STORE</a>
        </div>
    </div>
    <div class="row">
        <div class="col-3 bg-info p-2">
            <h4 class="text-white m-2">Categories</h4>
        </div>
        <div class="col-9 bg-success p-2">
            <h4 class="text-white m-2">Products</h4>
        </div>
    </div>
</div>
</template>

然后在App.vue文件中修改为如下内容:

<template>
<store />
</template>

<script>
import Store from './components/Store'

export default {
    name: 'App',
    components: {
        Store
    }
}
</script>

<style>
#app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
}
</style>

需要先引入组件文件,然后在export default里的components里添加组件,最后在template模板标签中添加相应的组件元素即可

当vue.js处理组件模板时,会使用组件的模板文件替换相应的组件元素标签

创建产品列表

在src/components文件夹中添加ProductList.vue文件,其内容如下

<template>
<div>
    <div v-for="p in products" v-bind:key="p.id" class="card m-1 p-1 bg-light">
        <h4>
            {{p.name}}
            <span class="badge badge-pill badge-primary float-right">
                {{p.price}}
            </span>
        </h4>
        <div class="card-text bg-white p-1">{{p.description}}</div>
    </div>
</div>
</template>

<script>
import {
    mapState
} from "vuex";

export default {
    computed: {
        ...mapState(["products"])
    },
}
</script>

添加产品列表到应用

在src/components文件夹的Store.vue文件中注册组件

<template>
<div class="container-fluid">
    <div class="row">
        <div class="col bg-dark text-white">
            <a class="navbar-brand">SPORTS STORE</a>
        </div>
    </div>
    <div class="row">
        <div class="col-3 bg-info p-2">
            <h4 class="text-white m-2">Categories</h4>
        </div>
        <div class="col-9 p-2">
            <!-- 使用注册组件 -->
            <product-list />
        </div>
    </div>
</div>
</template>

<script>
// 导入组件文件
import ProductList from "./ProductList"
export default {
    components: {
        ProductList // 注册组件
    }
}
</script>

过滤价格数据

在src/components文件夹的ProductList.vue文件中添加如下内容

<template>
<div>
    <div v-for="p in products" v-bind:key="p.id" class="card m-1 p-1 bg-light">
        <h4>
            {{p.name}}
            <span class="badge badge-pill badge-primary float-right">
                {{p.price | currency}} <!--使用过滤函数 -->
            </span>
        </h4>
        <div class="card-text bg-white p-1">{{p.description}}</div>
    </div>
</div>
</template>

<script>
import {
    mapState
} from "vuex";

export default {
    computed: {
        ...mapState(["products"])
    },
    // 添加过滤函数
    filters: {
        currency(value) {
            return new Intl.NumberFormat("en-US", {
                style: "currency",
                currency: "USD"
            }).format(value);
        }
    }
}
</script>

使用过滤函数通过通道|(竖线)进行传递数据

添加产品分页

在src/store文件夹的index.js文件中进行分页

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const testData = [];

for(let i = 1; i <= 10; i++){
    testData.push({
        id: i, name: `Product #${i}`, category: `Category ${i % 3}`,
        description: `This is Product #${i}`, price: i * 50
    })
}

export default new Vuex.Store({
    strict: true,
    state: {
        products: testData,
        productsTotal: testData.length,
        currentPage: 1,
        pageSize: 4
    },
    getters: {
        // 根据当前分页以及每页个数返回当前页面产品列表
        processedProducts: state => {
            let index = (state.currentPage - 1) * state.pageSize;
            return state.products.slice(index, index + state.pageSize);
        },
        // 计算分页总数
        pageCount: state => Math.ceil(state.productsTotal / state.pageSize)
    },
    mutations: {
        // 设置当前分页
        setCurrentPage(state, page){
            state.currentPage = page;
        },
        // 设置每页个数
        setPageSize(state, size){
            state.pageSize = size;
            state.currentPage = 1;
        }
    }
})
  • mutations 时一个改变数据方法的集合

接下来在src/components文件夹中添加文件PageControls.vue文件,其内容如下:

<template>
  <div v-if="pageCount > 1" class="text-right">
    <div class="btn-group mx-2">
      <button
        v-for="i in pageNumbers"
        v-bind:key="i"
        class="btn btn-secpmdary"
        v-bind:class="{ 'btn-primary': i == currentPage }"
      >
        {{ i }}
      </button>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters } from "vuex";

export default {
  computed: {
    ...mapState(["currentPage"]),
    ...mapGetters(["pageCount"]),
    pageNumbers() {
      return [...Array(this.pageCount + 1).keys()].slice(1);
    },
  },
};
</script>

然后在src/components文件夹中的ProductList.vue文件中使用分页

<template>
<div>
    <div v-for="p in products" v-bind:key="p.id" class="card m-1 p-1 bg-light">
        <h4>
            {{p.name}}
            <span class="badge badge-pill badge-primary float-right">
                <!--使用过滤函数 -->
                {{p.price | currency}}
            </span>
        </h4>
        <div class="card-text bg-white p-1">{{p.description}}</div>
    </div>
 <page-controls />
</div>
</template>

<script>
import {
    mapGetters
} from "vuex";
import PageControls from "./PageControls";

export default {
    components: {
        PageControls
    },
    computed: {
        ...mapGetters({
            products: "processedProducts"
        })
    },
    // 添加过滤函数
    filters: {
        currency(value) {
            return new Intl.NumberFormat("en-US", {
                style: "currency",
                currency: "USD"
            }).format(value);
        }
    }
}
</script>

切换产品页面

在src/components文件夹中的PageControls.vue文件中添加如下内容:

<template>
<div v-if="pageCount > 1" class="text-right">
    <div class="btn-group mx-2">
        <!-- 使用v-on指令绑定点击事件 -->
        <button v-for="i in pageNumbers" v-bind:key="i" class="btn btn-secpmdary" v-bind:class="{ 'btn-primary': i == currentPage }" v-on:click="setCurrentPage(i)">
            {{ i }}
        </button>
    </div>
</div>
</template>

<script>
import {
    mapState,
    mapGetters,
    mapMutations
} from "vuex";

export default {
    computed: {
        ...mapState(["currentPage"]),
        ...mapGetters(["pageCount"]),
        pageNumbers() {
            return [...Array(this.pageCount + 1).keys()].slice(1);
        },
    },
    methods: {
        ...mapMutations(["setCurrentPage"])
    }
};
</script>

改变每页个数

在src/components文件夹的PageControls.vue文件中添加如下内容:

<template>
<div class="row mt-2">
    <div class="col form-group">
        <select class="form-control" v-on:change="changePageSize">
            <option value="4">4 per page</option>
            <option value="8">8 per page</option>
            <option value="12">12 per page</option>
        </select>
    </div>
    <div class="text-right col">
        <div class="btn-group mx-2">
            <!-- 使用v-on指令绑定点击事件 -->
            <button v-for="i in pageNumbers" v-bind:key="i" class="btn btn-secpmdary" v-bind:class="{ 'btn-primary': i == currentPage }" v-on:click="setCurrentPage(i)">
                {{ i }}
            </button>
        </div>
    </div>
</div>
</template>

<script>
import {
    mapState,
    mapGetters,
    mapMutations
} from "vuex";

export default {
    computed: {
        ...mapState(["currentPage"]),
        ...mapGetters(["pageCount"]),
        pageNumbers() {
            return [...Array(this.pageCount + 1).keys()].slice(1);
        },
    },
    methods: {
        ...mapMutations(["setCurrentPage", "setPageSize"]),
        changePageSize($event) {
            this.setPageSize(Number($event.target.value));
        }
    }
};
</script>

添加分类选项

在src/store文件夹中index.js中添加如下内容:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const testData = [];

for(let i = 1; i <= 10; i++){
    testData.push({
        id: i, name: `Product #${i}`, category: `Category ${i % 3}`,
        description: `This is Product #${i}`, price: i * 50
    })
}

export default new Vuex.Store({
    strict: true,
    state: {
        products: testData,
        productsTotal: testData.length,
        currentPage: 1,
        pageSize: 4,
        currentCategory: "All"
    },
    getters: {
        productsFilteredByCategory: state => state.products.filter(p => state.currentCategory == "All" || p.category == state.currentCategory),
        // 根据当前分页以及每页个数返回当前页面产品列表
        processedProducts: (state, getters) => {
            let index = (state.currentPage - 1) * state.pageSize;
            return getters.productsFilteredByCategory.slice(index, index + state.pageSize);
        },
        // 计算分页总数
        pageCount: (state, getters) => Math.ceil(getters.productsFilteredByCategory.length / state.pageSize),
        categories: state => ["All", ...new Set(state.products.map(p => p.category).sort())]
    },
    mutations: {
        // 设置当前分页
        setCurrentPage(state, page){
            state.currentPage = page;
        },
        // 设置每页个数
        setPageSize(state, size){
            state.pageSize = size;
            state.currentPage = 1;
        },
        // 设置当前分类
        setCurrentCategory(state, category){
            state.currentCategory = category;
            state.currentPage = 1;
        }
    }
})

在src/components文件夹中添加CategoryControls.vue文件,其内容如下:

<template>
<div class="container-fluid">
    <div class="row my-2" v-for="c in categories" v-bind:key="c">
        <button class="btn btn-block" v-on:click="setCurrentCategory(c)" v-bind:class="c == currentCategory ? 'btn-primary' : 'btn-secondary'">
            {{c}}
        </button>
    </div>
</div>
</template>

<script>
import {
    mapState,
    mapGetters,
    mapMutations
} from "vuex";
export default {
    computed: {
        ...mapState(["currentCategory"]),
        ...mapGetters(["categories"])
    },
    methods: {
        ...mapMutations(["setCurrentCategory"])
    }
}
</script>

在src/components文件夹的Store.vue中添加分类选择

<template>
<div class="container-fluid">
    <div class="row">
        <div class="col bg-dark text-white">
            <a class="navbar-brand">SPORTS STORE</a>
        </div>
    </div>
    <div class="row">
        <div class="col-3 bg-info p-2">
            <!-- 使用分类组件 -->
            <CategoryControls />
        </div>
        <div class="col-9 p-2">
            <!-- 使用注册组件 -->
            <product-list />
        </div>
    </div>
</div>
</template>

<script>
// 导入组件文件
import ProductList from "./ProductList"
import CategoryControls from "./CategoryControls";
export default {
    components: {
        ProductList,
        CategoryControls
    }
}
</script>

使用RESTful web服务

在src/store文件夹的index.js文件中添加如下内容

import Vue from "vue";
import Vuex from "vuex";

import Axios from "axios";

Vue.use(Vuex);

const baseUrl = "http://localhost:3500";
const productsUrl = `${baseUrl}/products`;
const categoriesUrl = `${baseUrl}/categories`;

const testData = [];

for(let i = 1; i <= 10; i++){
    testData.push({
        id: i, name: `Product #${i}`, category: `Category ${i % 3}`,
        description: `This is Product #${i}`, price: i * 50
    })
}

export default new Vuex.Store({
    strict: true,
    state: {
        products: testData,
        categoriesData: [],
        productsTotal: testData.length,
        currentPage: 1,
        pageSize: 4,
        currentCategory: "All"
    },
    getters: {
        productsFilteredByCategory: state => state.products.filter(p => state.currentCategory == "All" || p.category == state.currentCategory),
        // 根据当前分页以及每页个数返回当前页面产品列表
        processedProducts: (state, getters) => {
            let index = (state.currentPage - 1) * state.pageSize;
            return getters.productsFilteredByCategory.slice(index, index + state.pageSize);
        },
        // 计算分页总数
        pageCount: (state, getters) => Math.ceil(getters.productsFilteredByCategory.length / state.pageSize),
        categories: state => ["All", ...state.categoriesData]
    },
    mutations: {
        // 设置当前分页
        setCurrentPage(state, page){
            state.currentPage = page;
        },
        // 设置每页个数
        setPageSize(state, size){
            state.pageSize = size;
            state.currentPage = 1;
        },
        // 设置当前分类
        setCurrentCategory(state, category){
            state.currentCategory = category;
            state.currentPage = 1;
        },
        setData(state, data){
            state.products = data.pdata;
            state.productsTotal = data.pdata.length;
            state.categoriesData = data.cdata.sort();
        }
    },
    actions: {
        async GamepadHapticActuator(context) {
            let pdata = (await Axios.get(productsUrl)).data;
            let cdata = (await Axios.get(categoriesUrl)).data;
            context.commit("setData", {pdata, cdata});
        }
    }
})

在App.vue中添加如下内容:

<template>
<store />
</template>

<script>
import Store from './components/Store';
import {
    mapActions
} from "vuex";

export default {
    name: 'App',
    components: {
        Store
    },
    methods: {
        ...mapActions({
            getData: "getData"
        })
    },
    created() {
        this.getData();
    }
}
</script>

<style>
#app {
    font-family: Avenir, Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    text-align: center;
    color: #2c3e50;
    margin-top: 60px;
}
</style>