尚品汇项目学习笔记
0、开发项目步骤
- 静态页面(HTML+CSS)
- 拆分组件
- 获取服务器的数据动态展示
- 完成相应业务逻辑
1、初始化项目与文件目录分析
需要 node+ webpack + 淘宝镜像
项目文件目录命令控制行输入:vue create 项目名称
public文件夹:静态资源,webpack进行打包的时候会原封不动打包到dist文件夹中。
pubilc/index.html:是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。
src文件夹(程序员代码文件夹)
assets: 存放公用的静态资源
components: 非路由组件(全局组件),其他组件放在views或者pages文件夹中
App.vue: 唯一的根组件,vue当中的组件.vue
main.js: 程序入口文件,最先执行的文件
babel.config.js: 配置文件(babel相关)
package.json: 项目的详细信息记录
package-lock.json: 缓存性文件(各种包的来源)
2、项目配置
2.1 项目运行,浏览器自动打开
package.json
"scripts": {
"serve": "vue-cli-service serve --open",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
报错:npm WARN saveError ENOENT: no such file or directory, open ‘/Users/....../package.json‘
原因:终端路径与项目路径不一致,导致找不到package.json文件
解决方法: cd 项目路径
(更改终端路径)
2.2 关闭eslint校验工具 (两种方法)
2.2.1 根目录下创建vue.config.js,进行配置
module.exports = {
//关闭eslint
lintOnSave: false
}
2.2.2 vue UI可视化界面关闭
2.3 src文件夹配置别名,创建jsconfig.json,用@/代替src/,exclude表示不可以使用该别名的文件
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
3、项目路由分析
Vue中使用vue-router去实现
前端所谓路由:KV键值对
key: URL(地址栏中的路径)
value: 相应的路由组件
注意:项目上中下结构
路由组件:
Home组件、Search组件 、Login组件、Refister组件
非路由组件:
Header组件【首页、搜索页】、
Footer组件【首页、搜索页】【登录与注册页没有】
4、组件页面样式
组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖
npm install --save less less-loader@5
如果想让组件识别less样式,则在组件中设置
<script scoped lang="less">
5、安装路由
npm install --save vue-router
–save:可以让你安装的依赖,在package.json文件当中进行记录
一定要注意版本兼容问题,这里遇到了一次错误,安装了vue-router之后,编译没有问题,页面变成空白
报错:Uncaught TypeError: (0 , vue__WEBPACK_IMPORTED_MODULE_0__.defineComponent) is not a function
原因:vue-router版本不兼容,自动下载了最新版"vue-router": "^4.0.14"
,
解决方法:降低vue-router的版本。
https://blog.csdn.net/qq_54353631/article/details/123598926?spm=1001.2014.3001.5501
6、创建路由组件
创建路由组件【一般放在views|pages文件夹】
components文件夹:经常放置非路由组件(共用全局组件)
pages文件夹:经常放置路由组件
7、配置路由
项目中配置的路由一般放置在router文件夹中:
创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册
报错:TypeError: Cannot read properties of undefined (reading ‘$createElement‘)
原因:单词拼写错误,在配置路由时,component写成components
解决方法:修改单词
https://blog.csdn.net/qq_62449212/article/details/123044736
8、路由传参
1、query、params
- query、params两个属性可以传递参数
- query参数:不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2
- query参数对应的路由信息 path: “/search”
- params参数:属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2
- params参数对应的路由信息要修改为path: “/search/:keyword” 这里的/:keyword就是一个params参数的占位符
2、传参方法
- 字符串形式
//第一种:字符串形式
this.$router.push("/search/" + this.keyword + "?k=" + this.keyword.toUpperCase());
- 模板字符串
//第二种:模板字符串
this.$router.push(`/search/${this.keyword}?k=${this.keyword.toUpperCase()}`)
注意: 上面字符串的传参方法可以看出params参数和’/'结合,query参数和?结合 http://localhost:8080/#/search/asa?k=ASA
上面url中asd为params的值,keyword=asd为query传递的值。
- 对象 (最常用)
//第三种:对象
this.$router.push({name:"search",params:{keyword:this.keyword},query:{k:this.keyword.toUpperCase()}})
以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path 。
3、传参问题
- 路由传递参数(对象写法)path是否可以结合params参数一起使用?
不可以:不能这样书写,程序会崩掉
- 如何指定params参数可传可不传?比如配置路由时,params参数已经占位了,但是路由跳转时就是不传递。
路径即地址栏URL会出现问题
如果路由path要求传递params参数,但是没有传递,会发现地址栏URL有问题,详情如下:
Search路由项的path已经指定要传一个keyword的params参数,如下所示:
path: "/search/:keyword",
执行下面进行路由跳转的代码:
this.$router.push({name:"Search",query:{keyword:this.keyword}})
当前跳转代码没有传递params参数
地址栏信息:http://localhost:8080/#/?keyword=asd
此时的地址信息少了/search
正常的地址栏信息: http://localhost:8080/#/search?keyword=asd
解决方法:可以通过改变path来指定params参数可传可不传
在配置路由时,在占位的后面加一个问号
path: "/search/:keyword?",?表示该参数可传可不传
- params可传可不传,但是如果传递的时空串,如何解决
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''}})
出现的问题和1中的问题相同,地址信息少了/search
解决方法: 加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
- 路由组件能不能传递props数据?
可以,但是只能传递params参数,具体知识为props属性 。
9、多次执行相同的push问题
编程式导航路由跳转到当前路由(参数不变), 多次执行会抛出NavigationDuplicated的警告错误?
例如:使用this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}})时,如果多次执行相同的push,控制台会出现警告。
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result)
执行一次上面代码:
多次执行出现警告:
- 注意:编程式导航(push|replace)才会有这种情况的异常,声明式导航是没有这种问题,因为声明式导航内部已经解决这种问题。
这种异常,对于程序没有任何影响的。 - 为什么会出现这种现象:
由于vue-router最新版本3.5.2,引入了promise,当传递参数多次且重复,会抛出异常,promise需要传递成功和失败两个参数,我们的push中没有传递,因此出现上面现象, - 解决方法
第一种解决方案:是给push函数,传入相应的成功的回调与失败的回调
this.$router.push({name:‘Search’,params:{keyword:"…"||undefined}},()=>{},()=>{})
后面两项分别代表执行成功和失败的回调函数。
这种写法治标不治本,将来在别的组件中push|replace,编程式导航还是会有类似错误
第二种解决方案:push是VueRouter.prototype的一个方法,在router中的index重写该方法即可
//1、先把VueRouter原型对象的push,保存一份
let originPush = VueRouter.prototype.push;
//2、重写push|replace
//第一个参数:告诉原来的push,跳转的目标位置和传递了哪些参数
//第二个参数:成功回调
//第三个参数:失败回调
VueRouter.prototype.push = function (location,resolve,reject){
if(resolve && reject){
originPush.call(this,location,resolve,reject)
}else{
originPush.call(this,location,() => {},() => {})
}
}
注意:call与apply区别
相同点:都可以调用函数一次,都可以篡改函数的上下文一次
不同点:call与apply传递参数,call传递参数用逗号隔开,apply方法执行,传递数组
10、全局组件定义
如果一些组件多次使用,可以注册为全局组件,只需要注册一次,就可以在任意地方使用。全局的配置都需要在main.js里面配置。
//三级联动组件--全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:哪一个组件
Vue.component(TypeNav.name,TypeNav);
在Home组件中使用该全局组件
<template>
<div>
<!-- 三级联动全局组件已经注册为全局组件,不需要import引入 -->
<TypeNav></TypeNav>
</div>
</template>
11、Home其他组件定义
Home页面的index.vue文件下
<template>
<div>
<!-- 三级联动全局组件已经注册为全局组件,因此不需要引入-->
<TypeNav/>
<!-- 轮播图列表-->
<ListContainer/>
<!-- 今日推荐-->
<Recommend/>
<!-- 商品排行-->
<Rank/>
<!-- 猜你喜欢-->
<Like/>
<!-- 楼层 -->
<Floor/>
<Floor/>
<!-- 商标-->
<Brand/>
</div>
</template>
<script>
import ListContainer from './ListContainer'
import Recommend from './Recommend'
import Rank from './Rank'
import Like from './Like'
import Floor from './Floor'
import Brand from './Brand'
export default {
name: "index",
components: {
ListContainer,
Recommend,
Rank,
Like,
Floor,
Brand,
}
}
</script>
<style scoped>
</style>
12、封装axios
在src根目录下创建api文件夹,创建request.js文件
//需要对axios进行二次封装
import axios from 'axios';
const request = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl
baseURL:"/api",
timeout:5000,
});
//请求拦截器:在发送请求之前,请求拦截器会监测到,可以在发出请求前做一些事情
request.interceptors.request.use((config)=>{
return config;
})
//相应拦截器
request.interceptors.response.use((res)=>{
//成功的回调函数
return res.data;
},(error)=>{
//失败的回调函数
return Promise.reject(new Error('faile'));
})
//对外暴露
export default request;
附:axios中文文档
https://www.kancloud.cn/yunye/axios/234845
13、前端解决跨域问题
-
什么是跨域问题?
浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域 -
端口和协议的不同,只能通过后台来解决
-
localhost和127.0.0.1虽然都指向本机,但也属于跨域
-
跨域限制:
1、无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
2、无法接触非同源网页的 DOM
3、无法向非同源地址发送 AJAX 请求(可以发送,但浏览器会拒绝接受响应)
解决方法:
本项目通过代理实现跨域:
在vue.config.js文件中进行配置,
在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。
devServer: {
proxy: {
//会把请求路径中的/api换为后面的代理服务器
'/api': {
//提供数据的服务器地址
target: 'http://gmall-h5-api.atguigu.cn',
}
},
}
问题:ValidationError: Invalid options object. Dev Server has been initialized using an options object that does not match the API schema.
原因:webpack版本配置问题
解决方法:按报错修改DevServer的配置。
问题:500 (Internal Server Error)
原因:服务器错误
解决方法:修改正确的服务器地址
14、请求接口统一封装
在api文件夹中创建index.js文件,封装所有请求。
将每一个请求封装成函数并暴露出去,组件只需调用相应函数,这样当接口比较多时,如果需要修改只需要修改该文件即可。
//当前这个模块:API进行统一管理
import request from "./request";
//三级联动的接口
///api/product/getBaseCategoryList get请求 无参数
//箭头函数简写形式
export const reqCategoryList = ()=> request({url:'/product/getBaseCategoryList',method:'get'});
当组件想要使用相关请求时,只需要导入并调用相关函数,如:
import {reqCateGoryList} from './api'
//发起请求
reqCateGoryList();
15、nprogress进度条插件
首先需要安装nprogress插件:npm install --save nprogress
打开一个页面时,常常伴随着一些进度条的展示,原理为:发起请求时开启进度条插件,请求响应成功则结束进度条插件,需要在api/request.js文件里配置。
//需要对axios进行二次封装
import axios from 'axios';
//引入进度条
import nprogress from 'nprogress';
//引入进度条的样式
import "nprogress/nprogress.css";
const request = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl
baseURL:"/api",
timeout:5000,
});
//请求拦截器:在发送请求之前,请求拦截器会监测到,可以在发出请求前做一些事情
request.interceptors.request.use((config)=>{
//进度条开始
nprogress.start();
return config;
})
//响应拦截器
request.interceptors.response.use((res)=>{
//成功的回调函数
// 进度条结束
nprogress.done();
return res.data;
},(error)=>{
//失败的回调函数
return Promise.reject(new Error('faile'));
})
//对外暴露
export default request;
noprogress.css可以修改background来改变进度条颜色:
#nprogress .bar {
background: #29d;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
16、引入Vuex
首先需要安装vuex:npm install --save vuex@3
根目录src下创建store文件夹,再创建index.js文件。
import Vue from 'vue';
import Vuex from 'vuex';
//需要使用插件一次
Vue.use(Vuex);
//对外暴露store类的一个实例
export default new Vuex.Store({
})
在main.js文件中引入并注册仓库:
import Vue from 'vue'
import App from './App.vue'
//三级联动组件--全局组件
import TypeNav from '@/components/TypeNav';
//第一个参数:全局组件名字,第二个参数:哪一个组件
Vue.component(TypeNav.name,TypeNav);
//引入路由
import router from '@/router';
//引入仓库
import store from './store';
new Vue({
render: h => h(App),
//注册路由 :下面的写法是KV一致省略V且router小写
router,
//注册仓库:组件实例会多一个$store属性
store,
}).$mount('#app')
16、动态展示三级联动数据
开发一个前端模块可以概括为以下几个步骤:
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state三连操作);
(4)组件获取仓库数据,动态展示;
在store文件夹下创建组件对应的小仓库文件夹与对应的index.js文件:
在index.js文件中进行配置:
//search仓库
//state :仓库存储数据的地方
const state = {};
//mutations :修改state的唯一手段
const mutations = {};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {};
//getters: 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};
//对外暴露
export default {
state,
mutations,
actions,
getters,
}
在主大仓库的index.js文件中进行配置:
import Vue from 'vue';
import Vuex from 'vuex';
//需要使用插件一次
Vue.use(Vuex);
import home from './home';
import search from './search';
//对外暴露store类的一个实例
export default new Vuex.Store({
modules:{
home,
search,
}
})
在组件的mounted中通知vuex发送请求
export default {
name: "TypeNav",
//组件挂载完毕:可以向服务器发送请求
mounted() {
//通知Vuex发送请求,获取数据,存储于仓库当中
this.$store.dispatch("categoryList");
},
再配置组件对应的仓库配置:
//home仓库
import {reqCategoryList} from '@/api';
//state :仓库存储数据的地方
const state = {
//根据获取到的数据再确定变量类型----------------1
categoryList:[],
};
//mutations :修改state的唯一手段
const mutations = {
//修改state里的数据---------------3
CATEGORYLIST(state,categoryList){
state.categoryList = categoryList;
}
};
// action:处理action,可以书写自己的业务逻辑,也可以处理异步
const actions = {
//通过API里面的接口函数,向服务器发送请求,获取服务器数据--------------------2
async categoryList({commit}){
let result = await reqCategoryList();
if (result.code==200){
commit("CATEGORYLIST",result.data)
}
}
};
//getters: 理解为计算属性,用于简化仓库数据,让组件获取仓库的数据更加方便
const getters = {};
export default {
state,
mutations,
actions,
getters,
}
这时候去看vuex后台就可以看见我们获取到的数据。
关于state可以浏览官网: 官网state介绍
组件获取相应数据
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from "vuex";
export default {
name: "TypeNav",
//组件挂载完毕:可以向服务器发送请求
mounted() {
//通知Vuex发送请求,获取数据,存储于仓库当中
this.$store.dispatch("categoryList");
},
computed: {
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
//右侧需要的是一个函数,当使用这个计算属性的时候,右侧函数会立即执行一次
//注入一个参数state,即为大仓库中的数据
categoryList: (state) => state.home.categoryList,
}),
},
};
最后通过v-for替换且动态展示模板里的数据 :
<div class="sort">
<div class="all-sort-list2">
<div
class="item"
v-for="(c1,index) in categoryList.slice(0, 16)"
:key="c1.categoryId"
>
<!--通过slice(0,16)获取前16条数据-->
<h3>
<a href="">{{ c1.categoryName }}</a>
</h3>
<div class="item-list clearfix">
<div
class="subitem"
v-for="(c2,index) in c1.categoryChild"
:key="c2.categoryId"
>
<dl class="fore">
<dt>
<a href="">{{ c2.categoryName }}</a>
</dt>
<dd>
<em v-for="(c3,index) in c2.categoryChild" :key="c3.categoryId">
<a href="">{{ c3.categoryName }}</a>
</em>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>