尚品汇项目学习笔记(待更新...)

2023-10-08 11:30

本文主要是介绍尚品汇项目学习笔记(待更新...),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原文链接:https://blog.csdn.net/weixin_43424325/article/details/121684101 

前端Vue核心

开发一个前端模块可以概括为以下几个步骤:
(1)写静态页面、拆分为静态组件;
(2)发请求(API);
(3)vuex(actions、mutations、state三连操作);
(4)组件获取仓库数据,动态展示;

1、vue文件目录分析

vue-cli脚手架初始化项目。

node + webpack + 淘宝镜像

node_modules:项目依赖文件夹

public:静态资源(图片),webpack进行打包的时候,会原封不动打包到dist文件夹中。

pubilc/index.html是一个模板文件,作用是生成项目的入口文件,webpack打包的js,css也会自动注入到该页面中。我们浏览器访问项目的时候就会默认打开生成好的index.html。

src(程序员代码文件夹)

assets: 放置静态资源(多个组件共用的静态资源),放置在assets文件夹里面静态资源,在webpack打包的时候,webpack会把静态资源当做一个模块,打包JS文件里面。
components: 非路由组件(全局组件),其他组件放在views或者pages文件夹中
App.vue: 唯一的根组件
main.js: 程序入口文件,最先执行的文件

babel.config.js:配置文件(babel相关)
package.json: 项目的详细信息记录。认为项目“身份证”,记录项目叫做什么、项目当中有哪些依赖、项目怎么运行。
package-lock.json: 缓存性文件(各种包的来源)

README.md:说明性文件

2、项目配置

2.1 项目运行,浏览器自动打开

package.json
"scripts": {"serve": "vue-cli-service serve --open","build": "vue-cli-service build","lint": "vue-cli-service lint"},

2.2 关闭eslint校验工具(不关闭会有各种规范,不按照规范就会报错)

  • 根目录下创建vue.config.js,进行配置
module.exports = {//关闭eslintlintOnSave: false}

2.3 src文件夹简写方法,配置别名。

创建jsconfig.json配置别名@提示【用@/代替src/,exclude表示不可以使用该别名的文件】

{"compilerOptions": {"baseUrl": "./","paths": {"@/*": ["src/*"]}},"exclude": ["node_modules", "dist"]
}

3、组件页面样式

组件页面的样式使用的是less样式,浏览器不识别该样式,需要下载相关依赖
npm install --save less less-loader@5
如果想让组件识别less样式,则在组件中设置
<script scoped lang="less">

4、清除vue页面默认的样式

vue是单页面开发,我们只需要修改public下的index.html文件

<link rel="stylesheet" href="reset.css">

5、路由组件的搭建

前端所谓路由:KV键值对

        key:URL(地址栏中的路径)

        value:相应的路由组件

安装路由:cnpm install --save vue-router@3

        --save:可以让你安装的依赖,在package.json文件当中进行记录

创建pages文件夹,并创建路由组件
5.1创建router文件夹,并创建index.js进行路由配置,最终在main.js中引入注册

5.2 总结
路由组件和非路由组件区别:

  1. 路由组件放在pages|views中,非路由组件放在components中
  2. 路由组件通过路由【在router文件夹中进行注册,使用的即为组件的名字】使用,非路由组件以标签的形式使用
  3. 在main.js注册完路由,所有的路由和非路由组件身上都会拥有$router、$route属性
  • $router:一般进行编程式导航进行路由跳转【push|replace】
  • $route: 一般获取路由信息【name、path、params等】

5.3 路由跳转方式

  • 声明式导航 :router-link(务必要有to属性) 它也可以加class修饰
  • 编程式导航 :利用的是组件实例的$router.push|replace方法,声明式导航能做的编程式都能做,而且还可以处理一些业务

6、Footer组件显示与隐藏

  • Footer在登录注册页面是不存在的,所以要隐藏,v-if 或者 v-show
  • 这里使用v-show,因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏
  • 配置路由的时候,可以给路由配置元信息meta,
  • 在路由的原信息中定义show属性,用来给v-show赋值,判断是否显示Footer组件

面试题:v-show与v-if区别?

        v-show:通过样式display控制

        v-if:通过元素上树与下树进行操作

面试题:开发项目的时候,优化手段有哪些?

        1:v-show|v-if

        2:按需加载

7、路由传参

7.1、query、params两个属性可以传递参数

query参数

不属于路径当中的一部分,类似于get请求,地址栏表现为 /search?k1=v1&k2=v2

query参数对应的路由信息 path: "/search"
params参数:

属于路径当中的一部分,需要注意,在配置路由的时候,需要占位 ,地址栏表现为 /search/v1/v2
params参数对应的路由信息要修改为path: "/search/:keyword" 这里的/:keyword就是一个params参数的占位符

7.2、传参方法

  • 字符串形式:this.$router.push(“/search/”+this.params传参+“?k=”+this.query传参)
  • 模板字符串:this.$router.push( ` /search/ {this.params传参}?k=${this.query传参}`)

     注意: 上面字符串的传参方法可以看出params参数和'/'结合,query参数和?结合
http://localhost:8080/#/search/asd?keyword=asd
上面url中asd为params的值,keyword=asd为query传递的值。

  • 对象(常用):this.$router.push({name:“路由名字”,params:{传参},query:{传参})。

以对象方式传参时,如果我们传参中使用了params,只能使用name,不能使用path,如果只是使用query传参,可以使用path 。

7.3 路由传参相关面试题

 (1)路由传递参数(对象写法)path是否可以结合params参数一起使用?

答:不可以。不能这样书写,程序会崩掉

(2)如何指定params参数可传可不传

答:在路由当中params后面加上一个? ,代表的是params参数可以传递,也可以不传递

path: "/search/:keyword?",?表示该参数可传可不传

参考连接:https://blog.csdn.net/weixin_44867717/article/details/109773945

(3)、params可传可不传,但是如果传递是空串,如何解决 。

答:加入||undefined,当我们传递的参数为空串时地址栏url也可以保持正常

this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})

(4)路由组件能不能传递props数据?
   可以,但是只能传递params参数,具体知识为props属性 。

8、多次执行相同的push问题

多次执行相同的push问题,控制台会出现警告
例如:使用this.$router.push({name:‘Search’,params:{keyword:“…”||undefined}})时,如果多次执行相同的push,控制台会出现警告。

let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result)

执行一次上面代码:

在这里插入图片描述

多次执行出现警告: 

在这里插入图片描述

原因:push是一个promise,promise需要传递成功和失败两个参数,我们的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){// call|apply区别// 相同点:都可以调用函数一次,都可以篡改函数的上下文一次// 不同点:call与apply传递参数:call传递参数用逗号隔开,apply方法执行,传递数组originPush.call(this.location,resolve,reject)}else{originPush.call(this.location,() => {},() => {})}
}

9、定义全局组件

我们的三级联动组件是全局组件,全局的配置都需要在main.js中配置

//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);

在Home组件中使用该全局组件

<template><div><!--  三级联动全局组件已经注册为全局组件,因此不需要引入--><TypeNav/></div>
</template>

全局组件可以在任一页面中直接使用,不需要导入声明
下面全部商品分类就是三级联动组件

在这里插入图片描述

10、代码改变时实现页面自动刷新 

根目录下vue.config.js文件设置

module.exports = {//关闭eslintlintOnSave: false,devServer: {// true 则热更新,false 则手动刷新,默认值为 trueinline: true,// development server port 8000port: 8001,}
}

注意:修改完该配置文件后,要重启一下项目

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

axios中文文档,包含详细信息。
使用说明 · Axios 中文说明 · 看云

axios二次封装:XMLHttpRequest、fetch、JQ、axios

12.1 为什么需要进行二次封装axios

请求拦截器、相应拦截器:

        请求拦截器,可以在发请求之前处理一些业务

        相应拦截器,当服务器数据返回以后,可以处理一些事情

12.2  axios如何封装?
在根目录下创建api文件夹,创建request.js文件。
内容如下,当前文件代码还比较少,后续有需求可以增添内容。

// 对axios进行二次封装
import axios from 'axios'// 1.利用axios对象的方法create,去创建一个axios实例
// 2.request就是axios,只不过稍微配置一下
const requests = axios.create({// 配置对象// 基础路径,发请求的时候,路径当中会出现apibaseURL: '/api',// 代表请求超时的时间5Stimeout: 5000
})
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {// config: 配置对象,对象里面有一个属性很重要,headers请求头//比如添加tokenreturn config
})// 相应拦截器
requests.interceptors.response.use((res) => {// 成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情return res.data
}, (error) => {// 响应失败的回调函数console.log("响应失败" + error)return Promise.reject(new Error('faile'))
})// 对外暴露
export default requests

13、前端通过代理解决跨域问题

在根目录下的vue.config.js中配置,proxy为通过代理解决跨域问题。
我们在封装axios的时候已经设置了baseURL为api,所以所有的请求都会携带/api,这里我们就将/api进行了转换。如果你的项目没有封装axios,或者没有配置baseURL,建议进行配置。要保证baseURL和这里的代理映射相同,此处都为’/api’。

module.exports = {//关闭eslintlintOnSave: false,devServer: {// true 则热更新,false 则手动刷新,默认值为 trueinline: false,// development server port 8000port: 8001,//代理服务器解决跨域proxy: {//会把请求路径中的/api换为后面的代理服务器'/api': {//提供数据的服务器地址target: 'http://gmall-h5-api.atguigu.cn',}},}
}

webpack官网相关知识解读
网站中的webpack.config.js就是vue.config.js文件。

14、请求接口统一封装

在文件夹api中创建index.js文件,用于封装所有请求
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,这样当我们的接口比较多时,如果需要修改只需要修改该文件即可。

如下所示:

//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/request";//首页三级分类接口
export const reqCateGoryList = () => {return  requests({url: '/product/getBaseCategoryList',method: 'GET'})
}

当组件想要使用相关请求时,只需要导入相关函数即可,以上图的reqCateGoryList 为例:

import {reqCateGoryList} from './api'
//发起请求
reqCateGoryList();

15、nprogress进度条插件

打开一个页面时,往往会伴随一些请求,并且会在页面上方出现进度条。它的原理时,在我们发起请求的时候开启进度条,在请求成功后关闭进度条,所以只需要在request.js中进行配置。
如下图所示,我们页面加载时发起了一个请求,此时页面上方出现蓝色进度条
在这里插入图片描述

在这里插入图片描述

对应的request.js设置 

// 对axios进行二次封装
import axios from 'axios'
// 引入进度条
import nprogress from 'nprogress'
// 引入进度条样式
import "nprogress/nprogress.css"
// start:进度条开始  done:进度条结束// 1.利用axios对象的方法create,去创建一个axios实例
// 2.request就是axios,只不过稍微配置一下
const requests = axios.create({// 配置对象// 基础路径,发请求的时候,路径当中会出现apibaseURL: '/api',// 代表请求超时的时间5Stimeout: 5000
})
// 请求拦截器:在发请求之前,请求拦截器可以检测到,可以在请求发出去之前做一些事情
requests.interceptors.request.use((config) => {// config: 配置对象,对象里面有一个属性很重要,headers请求头//比如添加token// 进度条开始nprogress.start()return config
})// 相应拦截器
requests.interceptors.response.use((res) => {// 成功的回调函数:服务器相应数据回来以后,响应拦截器可以检测到,可以做一些事情// 进度条结束nprogress.done()return res.data
}, (error) => {// 响应失败的回调函数console.log("响应失败" + error)return Promise.reject(new Error('faile'))
})// 对外暴露
export default requests

可以通过修改nprogress.css文件的background来修改进度条颜色。

在这里插入图片描述

16、手动引入vuex

vuex状态管理库

16.1 vuex是什么?

vuex是官方提供的一个插件,状态管理库,集中式管理项目中组件共用的数据。

切记,并不是全部项目都需要vuex,如果项目很小,完全不需要vuex,如果项目很大,组件很多、数据很多,数据维护很费劲,vuex

state  mutations  actions getters modules

16.2 vuex基本使用

首先确保安装了vuex,根目录创建store文件夹,文件夹下创建index.js,内容如下:

import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex)//对外暴露store的一个实例
export default new Vuex.Store({state:{},mutations:{},actions:{},})

如果想要使用vuex,还要再main.js中引入
main.js:
(1) 引入文件
(2) 注册store
但凡是在main.js中的Vue实例中注册的实体,在所有的组件中都会有(this.$.实体名)属性

import store from './store'
new Vue({render: h => h(App),//注册路由,此时组件中都会拥有$router $route属性router,//注册store,此时组件中都会拥有$storestore
}).$mount('#app')

17、async await使用

如果我们没有封装请求api,而是直接调用axios,就不需要使用async await。
案例:我们将一个axios请求封装为了函数,我们在下面代码中调用了该函数:

import {reqCateGoryList} from '@/api'
export default {actions:{categoryList(){let result =  reqCateGoryList()console.log(result)}}
}

浏览器结果

在这里插入图片描述

返回了一个promise,证明这是一个promise请求,但是我们想要的是图片中的data数据。
没有将函数封装前我们都会通过then()回调函数拿到服务器返回的数据,现在我们将其封装了,依然可以使用then获取数据,代码如下

actions:{categoryList(){let result =  reqCateGoryList().then(res=>{console.log("res")console.log(res)return res})console.log("result")console.log(result)}}

结果

在这里插入图片描述

由于我们的promis是异步请求,我们发现请求需要花费时间,但是它是异步的,所有后面的console.log(“result”);console.log(result)会先执行,等我们的请求得到响应后,才执行console.log(“res”);console.log(res),这也符合异步的原则,但是我们如果在请求下面啊执行的是将那个请求的结果赋值给某个变量,这样就会导致被赋值的变量先执行,并且赋值为undefine,因为此时promise还没有完成。
在这里插入图片描述

所以我们引入了async await,async写在函数名前,await卸载api函数前面。await含义是async标识的函数体内的并且在await标识代码后面的代码先等待await标识的异步请求执行完,再执行。这也使得只有reqCateGoryList执行完,result 得到返回值后,才会执行后面的输出操作。

   async categoryList(){let result = await reqCateGoryList()console.log("result")console.log(result)}

结果

在这里插入图片描述

18、vuex(*****)

state、actions、mutations、getters的辅助函数使用,当多次访问store中的上述属性时,要使用个属性的辅助函数,可以减少代码量。

在使用上面的函数时,如果需要传递多个参数,需要把多个参数组合为一个对象传入(vuex是不允许多个参数分开传递的)

async addOrUpdateShopCart({commit},{skuId,skuNum}){let result = await reqAddOrUpdateShopCart(skuId,skuNum)console.log(result)if(result.data ===  200){}

 辅助函数官网链接
注意使用action时,函数的第一个参数,必须是{commit},即使不涉及到mutations操作,也必须加上该参数,否则会报错。

19、lodash插件防抖和节流

在进行窗口的resize、scroll,输入框内容校验等操作时,如果事件处理函数调用的频率无限制,会加重浏览器的负担,导致用户体验非常糟糕。此时我们可以采用debounce(防抖)和throttle(节流)的方式来减少调用频率,同时又不影响实际效果。
安装lodash插件,该插件提供了防抖和节流的函数,我们可以引入js文件,直接调用。当然也可以自己写防抖和节流的函数

lodash官网
防抖函数
节流函数
防抖(debounce):【回城,被打断就要重新来】用户操作很频繁,但是只执行一次,减少业务负担。

节流(throttle):【技能CD,CD没好,你用不了技能】用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
防抖和节流简述

例如:下面代码就是将changeIndex设置了节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。

import {throttle} from 'lodash'methods: {//鼠标进入修改响应元素的背景颜色//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次changeIndex: throttle(function (index){this.currentIndex = index},50),//鼠标移除触发时间leaveIndex(){this.currentIndex = -1}}

函数防抖与节流*******面试题

正常:事件触发非常频繁,而且每一次的触发,回调函数都要去执行(如果时间很短,而回调函数内部有计算,那么很可能出现浏览器卡顿)

防抖:前面的所有的触发都被取消,最后一次执行在规定的时间之后才会触发,也就是说如果连续快速的触发,只会执行最后一次

节流:在规定的间隔时间范围内不会重复触发回调,只有大于这个时间间隔才会触发回调,把频繁触发变为少量触发

今晚需要把防抖与节流的原理,通过JS实现【闭包 + 延迟器】

20、编程式导航+事件委托实现路由跳转

如上图所示,三级标签列表有很多,每一个标签都是一个页面链接,我们要实现通过点击表现进行路由跳转。
路由跳转的两种方法:导航式路由,编程式路由。

对于导航式路由,我们有多少个a标签就会生成多少个router-link标签,这样当我们频繁操作时会出现卡顿现象。
对于编程式路由,我们是通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。 

上面两种方法无论采用哪一种,都会影响性能。我们提出一种:编程时导航+事件委派 的方式实现路由跳转。事件委派即把子节点的触发事件都委托给父节点。这样只需要一个回调函数goSearch就可以解决。
事件委派问题:
(1)如何确定我们点击的一定是a标签呢?如何保证我们只能通过点击a标签才跳转呢?
(2)如何获取子节点标签的商品名称和商品id(我们是通过商品名称和商品id进行页面跳转的)

解决方法:
对于问题1:为三个等级的a标签添加自定义属性date-categoryName绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。

对于问题2:为三个等级的a标签再添加自定义属性data-category1Id、data-category2Id、data-category3Id来获取三个等级a标签的商品id,用于路由跳转。
我们可以通过在函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过dataset属性获取节点的属性信息。

 <div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex"><div class="item"  v-for="(c1,index) in categoryList" v-show="index!==16" :key="c1.categoryId" :class="{cur:currentIndex===index}"><h3 @mouseenter="changeIndex(index)"  ><a :data-categoryName="c1.categoryName" :data-category1Id="c1.categoryId" >{{c1.categoryName}}</a></h3><div class="item-list clearfix" :style="{display:currentIndex===index?'block':'none'}"><div class="subitem" v-for="(c2,index) in c1.categoryChild" :key="c2.categoryId"><dl class="fore"><dt><a :data-categoryName="c2.categoryName" :data-category2Id="c2.categoryId">{{c2.categoryName}}</a></dt><dd><em v-for="(c3,index) in c2.categoryChild"  :key="c3.categoryId"><a :data-categoryName="c2.categoryName" :data-category3Id="c3.categoryId">{{c3.categoryName}}</a></em>
</dd></dl></div></div></div></div>

注意:event是系统属性,所以我们只需要在函数定义的时候作为参数传入,在函数使用的时候不需要传入该参数。

//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
goSearch(event){console.log(event.target)}

在这里插入图片描述

对应的goSearrch函数

goSearch(event){let element = event.target//html中会把大写转为小写//获取目前鼠标点击标签的categoryname,category1id,category2id,category3id,// 通过四个属性是否存在来判断是否为a标签,以及属于哪一个等级的a标签let {categoryname,category1id,category2id,category3id} = element.dataset//categoryname存在,表示为a标签if(categoryname){//category1id一级a标签//整理路由跳转的参数let location = {name:'Search'}//跳转路由namelet query = {categoryName:categoryname}//路由参数if(category1id){query.category1Id = category1id}else if(category2id){//category2id二级a标签query.category2Id = category2id}else if(category3id){//category3id三级a标签query.category3Id = category3id}//整理完参数location.query = query//路由跳转this.$router.push(location)}},

21、Vue路由销毁问题

Vue在路由切换的时候会销毁旧路由。
我们在三级列表全局组件TypeNav中的mounted进行了请求一次商品分类列表数据。
由于Vue在路由切换的时候会销毁旧路由,当我们再次使用三级列表全局组件时还会发一次请求。
如下图所示:当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。

在这里插入图片描述

 由于信息都是一样的,出于性能的考虑我们希望该数据只请求一次,所以我们把这次请求放在App.vue的mounted中。(根组件App.vue的mounted只会执行一次)
注意:虽然main.js也是只执行一次,但是不可以放在main.js中。因为只有组件的身上才会有$store属性。

22、mock插件使用 mock.js

mock用来拦截前端ajax请求,返回我么们自定义的数据用于测试前端接口。
将不同的数据类型封装为不同的json文件,创建mockServe.js文件

banner、floor分别为轮播图和页面底部的假数据。
mockServe.js文件

// 先引入mockjs模块
import Mock from 'mockjs'
// 把JSON数据格式引入进来
//webpack默认对外暴露:json、图片
import banner from './banner.json'
import floor from './floor.json'
//mock数据:第一个参数请求地址、第二个参数:请求数据
Mock.mock('/mock/banner', { code: 200, data: banner })
Mock.mock('/mock/floor', { code: 200, data: floor })//记得要在main.js中引入一下
// 引入mockServe.js   ---- mock数据
import '@/mock/mockServe'

23、vuex数据存储与使用

我们会把公共的数据放在store中,然后使用时再去store中取。
以我们的首页轮播图数据为例。
1、在轮播图组件ListContainer.vue组件加载完毕后发起轮播图数据请求。

 mounted() {this.$store.dispatch("getBannerList")},

2、请求实际是在store中的actions中完成的

actions:{//获取首页轮播图数据async getBannerList({commit}){let result = await reqGetBannerList()if(result.code ===  200){commit("GETBANNERLIST",result.data)}}}

3、获取到数据后存入store仓库,在mutations完成。

state: {bannerList:[] // 轮播图的数据
}
//唯一修改state的部分
mutations: {GETBANNERLIST(state,bannerList){state.bannerList = bannerList}
}

4、轮播图组件ListContainer.vue组件在store中获取轮播图数据。由于在这个数据是通过异步请求获得的,所以我们要通过计算属性computed获取轮播图数据。
ListContainer.vue代码

<script>
import { mapState } from "vuex";
export default {name:'ListContainer',mounted() {// 派发action:通过vuex发起ajax请求,将数据存储在仓库当中this.$store.dispatch("getBannerList");},computed: {...mapState({bannerList: (state) => state.home.bannerList,}),},
};
</script>

总结:只要是公共数据都会放在store中,之后的实现步骤就是上面的固定步骤。

24、swiper插件实现轮播图

swiper官网
官网中给出了代码实例:
做一个简要总结:(代码可以直接复制本小节最后面的代码)

(1)安装swiper  【注意:安装swiper@5版本】
(2)在需要使用轮播图的组件内导入swpier和它的css样式
(3)在组件中创建swiper需要的dom标签(html代码,参考官网代码)
(4)创建swiper实例

注意:在创建swiper对象时,我们会传递一个参数用于获取展示轮播图的DOM元素,官网直接通过class(而且这个class不能修改,是swiper的css文件自带的)获取。但是这样有缺点:当页面中有多个轮播图时,因为它们使用了相同的class修饰的DOM,就会出现所有的swiper使用同样的数据,这肯定不是我们希望看到的。
解决方法:在轮播图最外层DOM中添加ref属性

<div class="swiper-container" id="mySwiper" ref="cur">
通过ref属性值获取DOM
let mySwiper = new Swiper(this.$refs.cur,{...})

 <!--banner轮播-->
<div class="swiper-container" id="mySwiper"><div class="swiper-wrapper"><divclass="swiper-slide"v-for="(carousel, index) in bannerList":key="carousel.id"><img :src="carousel.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div>
</div>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'
</script>

接下来要考虑的是什么时候去加载这个swiper,我们第一时间想到的是在mounted中创建这个实例。但是会出现无法加载轮播图片的问题。

原因:

我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。

因为dispatch当中涉及到异步语句,导致v-for遍历的时候结构还没有完全,因此不行

由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。

mounted() {//请求数据this.$store.dispatch("getBannerList")//创建swiper实例let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{pagination:{el: '.swiper-pagination',clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',}})},

解决方法一:等我们的数据请求完毕后再创建swiper实例。只需要加一个1000ms时间延迟再创建swiper实例.。将上面代码改为:

mounted() {this.$store.dispatch("getBannerList")setTimeout(()=>{let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{pagination:{el: '.swiper-pagination',clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',}})},1000)},

方法一肯定不是最好的,但是我们开发的第一要义就是实现功能,之后再完善。

解决方法二:我们可以使用watch监听bannerList轮播图列表属性,因为bannerList初始值为空,当它有数据时,我们就可以创建swiper对象

watch:{bannerList(newValue,oldValue){let mySwiper = new Swiper(this.$refs.cur,{pagination:{el: '.swiper-pagination',clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',}})}}

即使这样也还是无法实现轮播图,原因是,我们轮播图的html中有v-for的循环,我们是通过v-for遍历bannerList中的图片数据,然后展示。我们的watch只能保证在bannerList变化时创建swiper对象,但是并不能保证此时v-for已经执行完了。假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片(因为swiper对象生效的前提是html即dom结构已经渲染好了)。

完美解决方案使用watch+this.$nextTick()
官方介绍:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

this. $nextTick它会将回调延迟到下次 DOM 更新循环之后执行(循环就是这里的v-for)。

$nextTick:可以保证页面中的结构一定是有的。经常和很多插件一起使用【都xu'yao】

完整代码

<template><!--列表--><div class="list-container"><div class="sortList clearfix"><div class="center"><!--banner轮播--><div class="swiper-container" id="mySwiper"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(carouse,index) in bannerList" :key="carouse.id"><img :src="carouse.imgUrl" /></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev" ></div><div class="swiper-button-next"></div></div></div></div></div></div>
</template>
<script>
//引入Swiper
import Swiper from 'swiper'
//引入Swiper样式
import 'swiper/css/swiper.css'import {mapState} from "vuex";export default {name: "index",//主键挂载完毕,ajax请求轮播图图片mounted() {this.$store.dispatch("getBannerList")},computed:{...mapState({//从仓库中获取轮播图数据bannerList: (state) => {return state.home.bannerList}})},watch:{bannerList(newValue,oldValue){//this.$nextTick()使用this.$nextTick(()=>{let mySwiper = new Swiper(document.getElementsByClassName("swiper-container"),{pagination:{el: '.swiper-pagination',clickable: true,},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',}})})}}
}
</script>

注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数,完全可以模仿上面的写法。

25、props父子组件通信

prop官方讲解
原理:父组件设置一个属性绑定要传递的数据
子组件props接受该属性值
本项目的
父组件:home文件下的index.js

<template>
<div>
//...省略
<!--  父组件通过自定义属性list给子组件传递数据--><Floor v-for="floor in floorList"  :key="floor.id" :list="floor"/>
<!--  商标--></div>
</template>

子组件:Floor下的index.vue

<template><!--楼层--><div class="floor">//...省略</div>
</template><script>
export default {name: "floor",
//子组件通过props属性接受父组件传递的数据props:['list']
}
</script>

上面两代码一看,发现父子组件竟然都是Floor组件,这使得我们对父子组件的概念难以理解。
个人理解1:
我们Floor文件夹下的index.vue创建了Floor组件,我们把它认为子组件。
我们在home文件夹下引用了该组件并使用了它,具体表现为<Floor v-for="floor in floorList" :key="floor.id" :list="floor"/>,此处使用的Floor标签,我们将其称为父组件。
个人理解2:
Floor是子组件,我们在home组件中调用了Floor,我们把home组件认为父组件,我们在home组件中实现了由home组件向Floor组件传递信息的操作,即父组件向子组件传递信息。

如下图所示:
第一张图是home组件的信息,我们的目的上将floorList中的数据分发给Floor组件。
在这里插入图片描述

通过前面描述的代码我们实现了父子通信,即将floorList分发给Floor组件。下图为Floor组件信息

在这里插入图片描述

对于父子组件的理解,我更偏向于个人理解二,因为它可以通过上面图片得到很好的解释。但是个人理解一对于新手理解起来更容易。

26、将轮播图模块提取为公共组件

需要注意的是我们要把定义swiper对象放在mounted中执行,并且还要设置immediate:true属性,这样可以实现,无论数据有没有变化,上来立即监听一次。

上一小节刚刚讲了props实现父组件向子组件传递消息,这里同样也会将轮播图列表传递给子组件,原理相同。
公共组件Carousel代码

<template><div class="swiper-container" ref="cur" id="floor1Swiper"><div class="swiper-wrapper"><div class="swiper-slide" v-for="(carouse,index) in carouselList" :key="carouse.id"><img :src="carouse.imgUrl"></div></div><!-- 如果需要分页器 --><div class="swiper-pagination"></div><!-- 如果需要导航按钮 --><div class="swiper-button-prev"></div><div class="swiper-button-next"></div></div>
</template><script>
import Swiper from "swiper";
import 'swiper/css/swiper.css'
export default {name: "Carousel",props:["carouselList"],watch: {carouselList: {//这里监听,无论数据有没有变化,上来立即监听一次immediate: true,//监听后执行的函数handler(){//第一次ListContainer中的轮播图Swiper定义是采用watch+ this.$nextTick()实现this.$nextTick(() => {let mySwiper = new Swiper(this.$refs.cur,{loop: true, // 循环模式选项// 如果需要分页器pagination: {el: '.swiper-pagination',// clickable: true},// 如果需要前进后退按钮navigation: {nextEl: '.swiper-button-next',prevEl: '.swiper-button-prev',}})})}}}
}
</script><style scoped></style>

在main.js中把Carousel组件设置为全局组件

import Carousel from '@/components/Carousel'
Vue.component(Carousel.name, Carousel)

Floor组件引用Carousel组件<Carousel :carouselList="list.carouselList"/>
ListContainer组件引用Carousel组件 <Carousel :carouselList="bannerList"/>

27、getters使用

getters是vuex store中的计算属性。
getters使用

如果不使用getters属性,我们在组件获取state中的数据表达式为:this.$store.state.子模块.属性
如果有多个组件需要用到此属性,我们要么复制这个表达式,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
个人理解:getters将获取store中的数据封装为函数,代码维护变得更简单(和我们将请求封装为api一样)。而且getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
注意:仓库中的getters是全局属性,是不分模块的。即store中所有模块的getter内的函数都可以通过$store.getters.函数名获取
下图为store内容

在这里插入图片描述

我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined。

store中search模块代码

import {reqGetSearchInfo} from '@/api';
const state = {searchList:{},
}
const mutations = {SEARCHLIST(state,searchList){state.searchList = searchList}
}
const actions = {//第二个参数data默认是一个空对象async getSearchListr({commit},data={}){let result = await reqGetSearchInfo(data)if(result.code === 200){commit("SEARCHLIST",result.data)}}
}
const getters = {goodsList(state){//网络出现故障时应该将返回值设置为空return state.searchList.goodsList||[]}
}
export default {state,mutations,actions,getters,
}

在Search组件中使用getters获取仓库数据

//只展示了使用getters的代码
<script>//引入mapGettersimport {mapGetters} from 'vuex'export default {name: 'Search',computed:{//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名...mapGetters(['goodsList'])}}
</script>

 后续数据的动态渲染就和之前模块相同,没有什么难度。

28、Object.asign实现对象拷贝

参考链接

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources)    【target:目标对象】,【souce:源对象(可多个)】
举个栗子:
const object1 = {a: 1,b: 2,c: 3
};const object2 = Object.assign({c: 4, d: 5}, object1);console.log(object2.c, object2.d);
console.log(object1)  // { a: 1, b: 2, c: 3 }
console.log(object2)  // { c: 3, d: 5, a: 1, b: 2 }注意:
1.如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后面的源对象的属性将类似地覆盖前面的源对象的属性
2.Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标
对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如
果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到
原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。

29、对象深拷贝

针对深拷贝,需要使用其他办法,因为 Object.assign()拷贝的是属性值。假如源对象的属性值是一个对象的引用,那么它也只指向那个引用。
let obj1 = { a: 0 , b: { c: 0}}; 
let obj2 = Object.assign({}, obj1); 
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj1.a = 1; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} 
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}} obj2.a = 2; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}} 
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 0}}obj2.b.c = 3; 
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}} 
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}} 
最后一次赋值的时候,b是值是对象的引用,只要修改任意一个,其他的也会受影响// Deep Clone (深拷贝)
obj1 = { a: 0 , b: { c: 0}}; 
let obj3 = JSON.parse(JSON.stringify(obj1)); 
obj1.a = 4; 
obj1.b.c = 4; 
console.log(JSON.stringify(obj3)); // { a: 0, b: { c: 0}}

30、利用路由信息变化实现动态搜索

最初想法:在每个三级列表和收缩按钮加一个点击触发事件,只要点击了就执行搜索函数。

这是一个很蠢的想法,如果这样就会生成很多回调函数,很耗性能。
最佳方法:我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。

如下图所示,$route是组件的属性,所以watch是可以监听的(watch可以监听组件data中所有的属性)
注意:组件中data的属性包括:自己定义的、系统自带的(如 $route)、父组件向子组件传递的等等。

在这里插入图片描述

search组件watch部分代码。
 

watch:{$route(newValue,oldValue){Object.assign(this.searchParams,this.$route.query,this.$route.params)this.searchInfo()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//所以每次请求结束后将相应参数制空this.searchParams.category1Id = '';this.searchParams.category2Id = '';this.searchParams.category3Id = '';this.$route.params.keyword = '';}},

31、面包屑相关操作

本次项目的面包屑操作主要就是两个删除逻辑。

分为:
当分类属性(query)删除时删除面包屑同时修改路由信息。
当搜索关键字(params)删除时删除面包屑、修改路由信息、同时删除输入框内的关键字。
1、query删除时
因为此部分在面包屑中是通过categoryName展示的,所以删除时应将该属性值制空或undefined。
可以通过路由再次跳转修改路由信息和url链接

//删除分类removeBread(){this.searchParams.categoryName = undefinedthis.$router.push({name:'Search',params:this.$route.params})},

2、params删除时
和query删除的唯一不同点是此部分会多一步操作:删除输入框内的关键字(因为params参数是从输入框内获取的)
输入框实在Header组件中的

在这里插入图片描述

header和search组件是兄弟组件,要实现该操作就要通过兄弟组件之间进行通信完成。

在这里插入图片描述

这里通过$bus实现header和search组件的通信。
$bus使用
(1)在main.js中注册

new Vue({//全局事件总线$bus配置beforeCreate() {//此处的this就是这个new Vue()对象//网络有很多bus通信总结,原理相同,换汤不换药Vue.prototype.$bus = this},render: h => h(App),//router2、注册路由,此时组件中都会拥有$router $route属性router,//注册store,此时组件中都会拥有$storestore
}).$mount('#app')

(2)search组件使用$bus通信,第一个参数可以理解为为通信的暗号,还可以有第二个参数(用于传递数据),我们这里只是用于通知header组件进行相应操作,所以没有设置第二个参数。

//删除搜索关键字removeBreadParams(){this.searchParams.keyword = undefined//通知兄弟组件header删除输入框的keyword关键字this.$bus.$emit("clear")if (this.$route.query) {this.$router.push({ name: "search", query: this.$route.query });}},

3)header组件接受$bus通信
注意:组件挂载时就监听clear事件

mounted() {//  组件挂载时就监听clear事件,clear事件在search模块中定义//  当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除this.$bus.$on("clear",()=>{this.keyword = ''})}

32、组件通信方式

第一种父子组件通信:

$ on、$emit自定义事件实现子组件给父组件传递信息。 props实现父组件给子组件传递数据。

第二种全局事件总线 $bus(适用于所有的场景)
第三种Vuex
第四中插槽(适用于父子组件通信)

组件通信方式连接

33、SearchSelector子组件传参及面包屑操作

31小节中描述了通过query、params参数生成面包屑,以及面包屑的删除操作对应地址栏url的修改。
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性。如下图所示

在这里插入图片描述

此处生成面包屑时会涉及到子组件向父组件传递信息操作(在32小节有相关知识点),之后的操作和前面31小姐讲的面包屑操作原理相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化回影响路由地址变化。
在具体的操作内还会涉及一些小的知识点,例如
字符串拼接 ·${}·,使用方法如下

var a = 1;
console.log(`a的值是:${a}`); //a的值是:1

至此面包屑部分内容结束。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑
判断searchParams相关属性是否存在,存在即显示。
面包屑删除逻辑
Search.vue js代码()

<script>import SearchSelector from './SearchSelector/SearchSelector'import {mapGetters} from 'vuex'export default {name: 'Search',components: {SearchSelector},data(){return{//动态获取searchParamssearchParams:{category1Id: "",//一级分类idcategory2Id: "",//二级分类idcategory3Id: "",//三级分类idcategoryName: "",keyword: "",order: "1:desc",pageNo: 1,pageSize: 10,props: [],//平台售卖属性trademark: ""//品牌},}},//在组件挂在之前动态编辑searchParams的值,因为组件挂在之后会使用到searchParamsbeforeMount() {//Object.assign方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。//Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象Object.assign(this.searchParams,this.$route.query,this.$route.params)},methods:{//搜索searchInfo(){this.$store.dispatch("getSearchListr",this.searchParams)},//删除分类(query)面包屑removeBread(){this.searchParams.categoryName = undefinedthis.$router.push({name:'Search',params:this.$route.params})},//删除搜索关键字(params)面包屑removeBreadParams(){this.searchParams.keyword = undefined//通知兄弟组件header删除输入框的keyword关键字this.$bus.$emit("clear")this.$router.push({name:'Search',query:this.$route.query})},//获取子组件传递的品牌信息(自定义事件)tradeMarkInfo(tradeMark){//接口文档中trademark的信息是"ID:品牌名称"形式this.searchParams.trademark = `${tradeMark.tmId}:${tradeMark.tmName}`this.searchInfo()},//删除品牌面包屑removeTradeMark(){this.searchParams.trademark = undefinedthis.searchInfo()},//获取子组件传递的属性信息(自定义事件)attrInfo(attr,attrValue){//searchParams.props元素为字符串形式,api文档有介绍let props = `${attr.attrId}:${attrValue}:${attr.attrName}`//数组去重if(this.searchParams.props.indexOf(props)===-1){this.searchParams.props.push(props)this.searchInfo()}},//删除属性面包屑removeAttr(index){this.searchParams.props.splice(index,1)}},mounted() {this.searchInfo()},computed:{...mapGetters(['goodsList'])},//watch可以监听组件上的属性watch:{$route:{handler(newValue,oldValue){console.log(this.$route)Object.assign(this.searchParams,this.$route.query,this.$route.params)this.searchInfo()//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数//所以每次请求结束后将相应参数制空this.searchParams.category1Id = '';this.searchParams.category2Id = '';this.searchParams.category3Id = '';},}},}
</script>

34、商品排序

排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串,例如‘1:asc’、‘2:desc’。1代表综合,2代表价格,asc代表升序,desc代表降序。

我们的升降序是通过箭头图标来辨别的,如图所示:

在这里插入图片描述

图标是iconfont网站的图标,通过引入在线css的方式引入图标

在这里插入图片描述

在public文件index引入该css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">

在search模块使用该图标

<div class="sui-navbar"><div class="navbar-inner filter"><ul class="sui-nav">
<!-- 这里isOne、isTwo、isAsc、isDesc是计算属性,如果不使用计算属性要在页面中写很长的代码--><li :class="{active:isOne}" @click="changeOrder('1')">
<!--                  阿里图标前置类iconfont--><a  >综合<span v-show="isOne" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"></span></a></li><li :class={active:isTwo} @click="changeOrder('2')"><a >价格<span v-show="isTwo" class="iconfont" :class="{'icon-up':isAsc,'icon-down':isDesc}"></span></a></li></ul></div></div>

isOne、isTwo、isAsc、isDesc计算属性代码

computed:{...mapGetters(['goodsList']),isOne(){return this.searchParams.order.indexOf('1')!==-1},isTwo(){return this.searchParams.order.indexOf('2')!==-1},isDesc(){return this.searchParams.order.indexOf('desc')!==-1},isAsc(){return this.searchParams.order.indexOf('asc')!==-1},},

点击‘综合’或‘价格’的触发函数changeOrder

//flag用于区分综合、价格,1:综合,2:价格changeOrder(flag){let newSearchOrder = this.searchParams.order//将order拆为两个字段orderFlag(1:2)、order(asc:desc)let orderFlag = this.searchParams.order.split(':')[0]let order = this.searchParams.order.split(':')[1]//由综合到价格、由价格到综合if(orderFlag!==flag){//点击的不是同一个按钮newSearchOrder = `${flag}:desc`this.searchInfo()}else{//多次点击的是不是同一个按钮newSearchOrder = `${flag}:${order==='desc'?'asc':'desc'}`}//需要给order重新赋值this.searchParams.order = newSearchOrder;//再次发请求this.searchInfo();}

35、手写分页器

实际开发中是不会手写的,一般都会用一些开源库封装好的分页,比如element ui。但是这个知识还是值得学习一下的。
核心属性:

pageNo(当前页码)、pageSize、total、continues(连续展示的页码)
核心逻辑是获取连续页码的起始页码和末尾页码,通过计算属性获得。(计算属性如果想返回多个数值,可以通过对象形式返回)

  //连续页码的起始页码、末尾页码startNumAndEnd(){let start = 0 , end = 0;//规定连续页码数字5(totalPage至少5页)//不正常现象if(this.continues > this.totalPage){start = 1end = this.totalPage}else{//正常现象      Math.floor:想下取整start = this.pageNo - Math.floor(this.continues/2)end = this.pageNo + Math.floor(this.continues/2)//start出现不正常现象纠正if(start < 1){start = 1end = this.continues}//end出现不正常现象纠正if(end > this.totalPage){end = this.totalPagestart = this.totalPage - this.continues + 1}}return {start,end}}

当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。

36、字符串拼接

如果你想在你的字符串内加入某个变量的值,就需要字符串拼接使用 ``(飘符号),由于 在markdown是单行代码标记所以下面我们用··代替。

这篇关于尚品汇项目学习笔记(待更新...)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/164953

相关文章

javafx 如何将项目打包为 Windows 的可执行文件exe

《javafx如何将项目打包为Windows的可执行文件exe》文章介绍了三种将JavaFX项目打包为.exe文件的方法:方法1使用jpackage(适用于JDK14及以上版本),方法2使用La... 目录方法 1:使用 jpackage(适用于 JDK 14 及更高版本)方法 2:使用 Launch4j(

Docker集成CI/CD的项目实践

《Docker集成CI/CD的项目实践》本文主要介绍了Docker集成CI/CD的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、引言1.1 什么是 CI/CD?1.2 docker 在 CI/CD 中的作用二、Docke

SpringBoot项目引入token设置方式

《SpringBoot项目引入token设置方式》本文详细介绍了JWT(JSONWebToken)的基本概念、结构、应用场景以及工作原理,通过动手实践,展示了如何在SpringBoot项目中实现JWT... 目录一. 先了解熟悉JWT(jsON Web Token)1. JSON Web Token是什么鬼

手把手教你idea中创建一个javaweb(webapp)项目详细图文教程

《手把手教你idea中创建一个javaweb(webapp)项目详细图文教程》:本文主要介绍如何使用IntelliJIDEA创建一个Maven项目,并配置Tomcat服务器进行运行,过程包括创建... 1.启动idea2.创建项目模板点击项目-新建项目-选择maven,显示如下页面输入项目名称,选择

Jenkins中自动化部署Spring Boot项目的全过程

《Jenkins中自动化部署SpringBoot项目的全过程》:本文主要介绍如何使用Jenkins从Git仓库拉取SpringBoot项目并进行自动化部署,通过配置Jenkins任务,实现项目的... 目录准备工作启动 Jenkins配置 Jenkins创建及配置任务源码管理构建触发器构建构建后操作构建任务

Ubuntu 24.04 LTS怎么关闭 Ubuntu Pro 更新提示弹窗?

《Ubuntu24.04LTS怎么关闭UbuntuPro更新提示弹窗?》Ubuntu每次开机都会弹窗提示安全更新,设置里最多只能取消自动下载,自动更新,但无法做到直接让自动更新的弹窗不出现,... 如果你正在使用 Ubuntu 24.04 LTS,可能会注意到——在使用「软件更新器」或运行 APT 命令时,

Nginx、Tomcat等项目部署问题以及解决流程

《Nginx、Tomcat等项目部署问题以及解决流程》本文总结了项目部署中常见的four类问题及其解决方法:Nginx未按预期显示结果、端口未开启、日志分析的重要性以及开发环境与生产环境运行结果不一致... 目录前言1. Nginx部署后未按预期显示结果1.1 查看Nginx的启动情况1.2 解决启动失败的

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template