从0到1打造属于自己的vue项目(webpack+vue2.0+axios+vue-router+vuex+express)并实现一个简易购物车案例
1.初始化项目:npm-init
项目结构:
2.webpack配置:参考webpack入门
完整实例:
(1)webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 插件:生成随机名称的文件自动引入在对应文件中
const {
CleanWebpackPlugin} = require('clean-webpack-plugin') //每次build自动清除上次build的文件
const MiniCssExtractPlugin = require('mini-css-extract-plugin') //这里需要说的细一点,上面我们所用到的mini-css-extract-plugin会将所有的css样式合并为一个css文件。如果你想拆分为一一对应的多个css文件,我们需要使用到extract-text-webpack-plugin,
// 而目前mini-css-extract-plugin还不支持此功能。我们需要安装@next版本的extract-text-webpack-plugin
const vueLoaderPlugin = require('vue-loader/lib/plugin')
const Webpack = require('webpack') //
const HappyPack = require('happypack') //HappyPack的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({
size:os.cpus().length})
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')
// let indexLess = new MinCssExtractPlugin('index.less');
// let indexCss = new MinCssExtractPlugin('index.css');
const CopyWebpackPlugin = require('copy-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin ///分析打包后的文件
module.exports = {
devServer:{
port:5000,
hot:true,
contentBase:'../dist',
progress: true, // 运行进度
proxy: {
'/api' : {
target: 'http://192.168.1.187:8887',
pathRewrite: {
'^/api' : ''},
changeOrigin: true, // target是域名的话,需要这个参数,
secure: false, // 设置支持https协议的代理
}
}
},
mode: "development", //开发模式 会将 DefinePlugin 中 process.env.NODE_ENV 的值设置为 development. 为模块和 chunk 启用有效的名。
entry: {
main: ["@babel/polyfill",path.resolve(__dirname,'../src/main.js')], //入口文件,被打包的文件
header: ["@babel/polyfill",path.resolve(__dirname,'../src/header.js')]
},
output: {
// filename: "mains.js", // 打包后的文件名称
filename: "[name].[hash:8].js",
path: path.resolve(__dirname,'../dist') //打包后的目录
},
plugins: [
new HtmlWebpackPlugin({
template: path.resolve(__dirname,'../public/index.html'), //打包生成的js文件已经被自动引入html文件中
filename: "index.html",
chunks: ['main']
}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname,'../public/header.html'), //打包生成的js文件已经被自动引入html文件中
filename: "header.html",
chunks: ['header']
}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: '[name].[hash].css',
chunkFilename: '[id].css',
}),
new vueLoaderPlugin(), //配置解析vue
new Webpack.HotModuleReplacementPlugin(), //配置热更新
// indexLess,
// indexCss
new HappyPack({
id: 'happyBabel',
loaders: [
{
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env']
],
cacheDirectory: true
}
}
],
threadPool: happyThreadPool //共享进程池
}),
new Webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./vendor-manifest.json')
}),
new CopyWebpackPlugin(
{
patterns: [
{
from: 'static', to:'static'}// 拷贝生成的文件到dist目录 这样每次不必手动去cv
]
}
),
new BundleAnalyzerPlugin({
analyzerHost: '127.0.0.1',
analyzerPort: 9992
})
],
module: {
rules: [
// {
// test: /\.ext$/,
// use: [
// 'cache-loader',
// ...loaders
// ],
// include: path.resolve(__dirname,'src')
// },
{
test: /\.(jpe?g|png|gif)$/i, //图片文件
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
fallback: {
loader: 'file-loader',
options: {
name: 'fonts/[name].[hash:8].[ext]'
}
}
}
}
]
},
{
test: /\.vue$/,
// use: ['vue-loader'],
use: ['cache-loader','vue-loader'],
// loader: 'vue-loader',
// include:[path.resolve(__dirname,'src')],
// exclude: /node_modules/
},
{
test: /\.js$/,
// loader: "babel-loader",
use:[{
loader:'happypack/loader?id=happyBabel'
}],
// options: {
// presets: ['@babel/preset-env']
// },
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['vue-style-loader','css-loader']
},
{
test: /\.less$/,
use:[ 'vue-style-loader','style-loader','css-loader',{
loader:'postcss-loader',
options:{
plugins:[require('autoprefixer')] // 版本过高会出错9.8.6
}
},'less-loader'] // 从右向左解析原则
// use: [
// MiniCssExtractPlugin.loader,
// 'css-loader',
// 'less-loader'
// ],
},
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js',
'@': path.resolve(__dirname,'../src'),
},
extensions:['*','.js','.json','.vue']
},
optimization: {
minimizer: [
new ParallelUglifyPlugin({
cacheDir: '.cache/',
uglifyJs: {
output: {
comments: false,
beautify: false
},
compress: {
drop_console: true,
collapse_vars: true,
reduce_vars: true
}
}
})
]
}
}
(2)webpack.dll.config.js
// 对于开发项目中不经常会变更的静态依赖文件。
// 类似于我们的elementUi、vue全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。
// 这样做的好处是每次更改我本地代码的文件的时候,webpack只需要打包我项目本身的文件代码,而不会再去编译第三方库。
// 以后只要我们不升级第三方包的时候,那么webpack就不会对这些库去打包,这样可以快速的提高打包的速度。
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
vendor: ['vue','element-ui']
},
output: {
path: path.resolve(__dirname,'static/js'), // 打包后文件输出位置
filename: '[name].dll.js',
library: '[name]_library', // 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '[name]-manifest.json'),
name: '[name]_library',
context: __dirname
})
]
}
3.下载安装vue、element-ui
npm i vue 官网链接:vue官网
npm i element-ui -S 官网链接:element-ui官网
在main.js中引入vue和element-ui
import Vue from "vue";
import App from "./App";
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import store from './store/index'
import router from '@/router/permission'
Vue.config.productionTip = false
Vue.use(ElementUI); //全局引用
new Vue({
store,
router,
render:h=> h(App)
}).$mount('#root')
4.配置axios
(1)安装axios
npm i axios 官网链接:axios中文网
在src目录下面新建一个utils文件夹,创建三个js文件:api.js、request.js、serveUrl.js
request.js: 添加拦截器和响应器
import axios from "axios"; //引入axios
const instance = axios.create({
timeout: 1000,
});
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
if(response.data.code === 200) {
return response;
}else{
return Promise.reject('服务器繁忙');
}
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
export default axios
serveUrl.js:管理配置请求url
const baseUrl = 'http://192.168.1.187:8887/api/'
const url = {
userInfo: baseUrl +'user',
shopList:baseUrl +'shopList'
}
export default url
api.js:
import url from './serveUrl'
import http from './request'
export function getUserInfo(data) {
return http({
url: url.userInfo,
method: 'get',
dataType: "json",
contentType: "application/x-www-form-urlencoded;charset=UTF-8",
data: data
})
}
export function getShop(data) {
return http({
url: url.shopList,
method: 'get',
dataType: "json",
contentType: "application/x-www-form-urlencoded;charset=UTF-8",
data: data
})
}
(2)编写简单的node接口
node结构如下:
1.初始化项目:npm init
2.安装express,npm i express --save
3.引入express
const http = require('http')
const express = require('express')
const app = express()
const shopList = [
{
name: 'iphone12紫色',
img:'https://img14.360buyimg.com/n0/s80x80_jfs/t1/172102/8/10639/201530/60a61e73E8c618926/79602404813e4b70.jpg.dpg',
price: 5999,
number:0,
total:0
},
{
name: 'iphone12紫色',
img:'https://img14.360buyimg.com/n0/s80x80_jfs/t1/172102/8/10639/201530/60a61e73E8c618926/79602404813e4b70.jpg.dpg',
price: 5999,
number:0,
total:0
},
{
name: 'iphone12紫色',
img:'https://img14.360buyimg.com/n0/s80x80_jfs/t1/172102/8/10639/201530/60a61e73E8c618926/79602404813e4b70.jpg.dpg',
price: 5999,
number:0,
total:0
}
]
const user = {
name: 'admim',
password: 123456
}
const data = {
data:[],
code: 200
}
// 配置跨域
app.all("*",function(req,res,next){
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin","*");
//允许的header类型
res.header("Access-Control-Allow-Headers","content-type");
//跨域允许的请求方式
res.header("Access-Control-Allow-Methods","DELETE,PUT,POST,GET,OPTIONS");
if (req.method.toLowerCase() == 'options') {
res.send(200); //让options尝试请求快速结束
}
else{
next();
}
})
app.get('/api/shopList',(req,res)=>{
console.log('请求了商品')
data.data = shopList
res.writeHeader(200,{
'Content-Type' : 'text/plain;charset=utf-8'}) ; //解决数据中乱码问题
res.end(JSON.stringify(data),'utf-8')
})
app.post('/api/login',(req,res)=>{
console.log(req,'req')
})
const serve = app.listen(8887,()=>{
console.log('服务器启动了')
})
启动项目: node app.js
5.配置vue-router
npm i vue-router
在src目录下新建一个router目录,并新建两个文件,permission.js和router.config.js
router.config.js:
import Vue from "vue";
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name:'Login',
component: () => import('@/views/login')
},
{
path: '/login',
name:'Login',
component: () => import('@/views/login')
},
{
path: '/home',
name:'Home',
meta:{
isLogin: true //是否需要登录
},
component:() => import('@/views/home')
},
{
path: '/car',
name:'Car',
meta:{
isLogin: false
},
component:() => import('@/views/my-car')
},
{
path: '/newcar',
name:'NewCar',
meta:{
isLogin: false
},
component:() => import('@/views/newcar')
}
]
// 将路径注入到Router中
const router = new VueRouter({
'mode': 'hash', //history模式404
scrollBehavior: () => ({
y: 0 }),
routes
})
export default router
permission.js: 全局路由守卫,一些跳转拦截验证,权限验证,白名单等配置
import router from "./router.config";
import store from "../store/index"
router.beforeEach((to,from,next)=>{
// 如果有用户的信息
if(store.state.user.username){
next();
}else if(to.matched.some(res=>res.meta.isLogin)){
//判断是否需要登录
if (sessionStorage['username']) {
next();
}else{
// 重定向到登录页
next({
path:"/login",
query:{
redirect:to.fullPath
}
});
}
}else{
next()
}
})
export default router;
在main.js中引入
import router from ‘@/router/permission’
new Vue({
router,
render:h=> h(App)
}).$mount(’#root’)
6.配置vuex
npm i vuex
在src下面创建一个store文件夹,创建一个index.js
在main.js和permission中引入
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
import {
getShop } from '../utils/api'
export default new Vuex.Store({
state: {
// state 类似 data
//这里面写入数据
shopList:[],
select:[],
user:{
username:'admin',
password:'123456'
}
},
getters: {
// getters 类似 computed
// 在这里面写个方法
totals(state) {
let total = 0
state.select.map((val)=>{
total += val.price*val.number
})
return total
}
},
mutations: {
// mutations 类似methods
// 写方法对数据做出更改(同步操作)
selectChange(state,select) {
state.select = select
},
getOneTotal(state,value) {
console.log(state)
state.shopList[value.$index].total = value.row.price*value.row.number
},
getShops(state,val) {
state.shopList = val
console.log(state,val,'+++')
},
deleteShop(state,value) {
state.shopList.splice(value.$index,1)
}
},
actions: {
// actions 类似methods
// 写方法对数据做出更改(异步操作)
}
})
在src下面创建一个login.vue,home.vue
login.vue:
<template>
<div>
<div class="login-container">
<div class="form">
<el-form :label-position="labelPosition" label-width="80px" :model="formLabelAlign" class="box">
<el-form-item label="用户名">
<el-input v-model="formLabelAlign.name"></el-input>
</el-form-item>
<el-form-item label="密码">
<el-input v-model="formLabelAlign.password" type="password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm()">立即登录</el-button>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script>
export default {
name: "login",
data() {
return {
labelPosition: 'right',
formLabelAlign: {
name: 'admin',
password: 123456,
}
}
},
methods:{
submitForm(){
//一般来说这里需要验证输入的内容是否合法,然后调用登录接口拿到token,再用token请求用户信息存在vuex中,
这样我们可以再permission中通过判断该页面是否需要登录以及是否含有用户信息进行验证是否可以跳转,这里简单
处理
this.$router.push({
path:'/home'})
}
}
}
</script>
<style scoped lang="less">
.center{
text-align: center;
}
.login-container{
width: 100%;
height: 100vh;
text-align: center;
.form{
display: inline-block;
vertical-align: middle;
width: 400px;
height: 50vh;
text-align: center;
padding: 40px;
.box{
margin-top:50px;
}
}
}
.login-container::after {
content: '';
display: inline-block;
height: 100%;
width: 0;
vertical-align: middle;
}
</style>
home.vue:
<template>
<div>
<router-link to="/car">购物车(普通版本)</router-link><br/>
<router-link to="/newcar">购物车(vuex版本)</router-link>
</div>
</template>
<script>
export default {
name: "home"
}
</script>
<style scoped>
</style>
7.实现简单购物车效果(普通版和vuex版)
在viewx下面新建my-car.vue和newcar.vue
my-car.vue:
<template>
<div>
<router-link to="/car">购物车(普通版本)</router-link><br/>
<router-link to="/newcar">购物车(vuex版本)</router-link>
</div>
</template>
<script>
export default {
name: "home"
}
</script>
<style scoped>
</style>
newcar.vue(vuex版本)
<template>
<div>
<h1 class="center">vuex版本</h1>
<div class="car-container">
<ul style="width: 100%">
<el-table
ref="multipleTable"
:data="tableData"
style="width: 80%"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
>
</el-table-column>
<el-table-column
label="商品"
width="250"
>
<template slot-scope="scope">
<div class="flex">
<div><img :src="scope.row.img" alt=""></div>
<div>{
{
scope.row.name}}</div>
</div>
</template>
</el-table-column>
<el-table-column
prop="price"
label="单价"
>
</el-table-column>
<el-table-column
prop="number"
label="数量"
>
<template slot-scope="scope">
<el-input-number v-model="scope.row.number" @change="handleChange(scope)" :min="0" size="small"></el-input-number>
</template>
</el-table-column>
<el-table-column
label="小计"
>
<template slot-scope="scope">{
{
scope.row.total }}</template>
</el-table-column>
<el-table-column
label="操作"
>
<template slot-scope="scope">
<el-button @click="deleteShops(scope)" type="danger" size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="total">总计:{
{
totals}}</div>
</ul>
</div>
</div>
</template>
<script>
import {
mapState,mapGetters,mapMutations,mapActions} from 'vuex';
import {
getShop } from '../utils/api'
export default {
name: "my-car",
data(){
return {
tableData: [
],
multipleSelection: [],
total:0
}
},
created() {
this.getShopList()
},
computed:{
...mapGetters(['totals'])
},
methods:{
...mapMutations(['selectChange','getOneTotal','getShops','deleteShop']),
getShopList() {
getShop().then((res)=>{
this.tableData = res.data.data
this.getShops(res.data.data)
}).catch((err)=>{
console.log(err)
})
},
handleSelectionChange(val) {
this.selectChange(val)
},
handleChange(value) {
this.getOneTotal(value)
},
deleteShops(value) {
this.deleteShop(value)
},
}
}
</script>
<style scoped lang="less">
.center{
text-align: center;
}
.flex{
display: flex;
justify-content: center;
align-items: center;
}
.car-container{
width: 1200px;
margin: 100px auto;
border: 1px solid #ccc;
.total{
width: 80%;
display: flex;
flex-direction: row-reverse;
}
}
</style>
最后npm run dev(保证node服务开启)
效果图: