新闻移动端练习知识点记录

目录

1.部分页面效果

2.环境

3.本项目遇到的知识点

3.1.长度尺寸单位vw

3.2 token鉴权

 3.3.个人中心

  3.4.路由守卫

3.5.axios部分设置 

 3.6.css穿透scoped修改子组件样式

  3.7.video标签使用 

 3.8.父组件中子组件标签上添加点击事件

  3.9.组件递归嵌套 

  3.10.vantUI使用(非按需引入)


1.部分页面效果

登录

首页

 文章详情

视频详情

2.环境

win7+vue.js+h5+vantUI

3.本项目遇到的知识点

3.1.长度尺寸单位vw

 使用vw实现页面尺寸适配

1vw 相当于1%的屏幕宽度

1vh 相当于1%的屏幕高度

如果用vw就永远只用宽作为标准,那么就可以指定所有的相对尺寸,因为vw有一个固定的参照物(宽度)

元素像素 / 设计稿宽度 * 100 = 以vw 为单位的长度数据

可以利用vscode插件px-to-vw自动转换px为vw,输入像素按alt+z即可转换

3.2 token鉴权

token和用户id放入本地存储,发送请求是在请求头传入

                       // 本地储存
                    localStorage.setItem('token',res.data.data.token);
                    localStorage.setItem('userId',res.data.data.user.id);
//headers 请求头中带上 ( JWT标准 json web token)
this.$axios({
    url,
    method,
    headers: {
        Authorization: "Bearer " + localStorage.getItem('token')
    }
}).then(res => {
    console.log(res.data);
    
})

  

 3.3.个人中心

 切换class

 通过判断性别来切换不同样式class

<i class="iconfont" :class="user.gender == 0 ? 'iconxingbienv col_pink':'iconxingbienan col_blue'" ></i>
<span :class="{heightlight:!obj.has_follow}" @click="handleFollow">{
   
   {obj.has_follow?'已关注':'关注'}}</span>
.heightlight{
     color:#fff !important;
     background-color: #ffc0cb !important;
}

 切换页面

$router.push()把路由添加到最后一位 

@click="$router.push({name:'editInfo'})"

 $router.replace()把路由替换到最后一位

this.$router.replace('/login');

  3.4.路由守卫

  路由守卫可以理解为页面跳转之前的拦截手段,可以在拦截时判断验证用户信息

 全局的前置路由守卫

  •  to 目标路由
  •  from 来源路由
  •  next 放行函数 

这里通过前置路由守卫来对页面的跳转进行拦截,当判断需要鉴权的页面没有token时,则跳转到登录页,有token则放行 

// 路由守卫
// 全局前置路由守卫,会在所有路由发生变化之前进行拦截
// 调用 router 的 beforeEach 方法
// router.beforeEach() 拦截所有跳转,跳转之前都会执行一个函数,我们要作为参数传进去
router.beforeEach((to,from,next)=>{
    // //写法一 直接判断 to.name
    // if (to.name == 'personalPage' || to.name == 'editPage') {

    // // 另外一种写法,是将需要校验的路由封装在一个数组中
    // var pageNeedAuth = [
    //   'editPage',
    //   'personalPage'
    // ]
    // // 如果在这个数组中能够找到 to.name 就证明需要校验
    // if(pageNeedAuth.indexOf(to.name) >= 0) {
    // 第三种判断方式, 直接在路由配置时, 添加 meta 数据
    // 就可以在这里进行判断
    if(to.meta.auth){
        const token = localStorage.getItem('token');
        if(token){
          next();
        }else{
          router.push('/login')
        }
    }else{
        next();
    }
});

  meta  在routes中自定义数据

  这里在路由中使用meta来设置需要鉴权的页面,辅助路由守卫鉴权

const routes = [
    {path:'/login',component:Login},
    {path:'/register',component:Register},
    {path:'/',component:index},
    {name:'myFollow',path:'/myFollow',component:MyFollow,meta:{auth:true}},
    {name:'personalCenter',path:'/personalCenter',component:PersonalCenter,meta:{auth:true}},
    {name:'editInfo',path:'/editInfo',component:EditInfo,meta:{auth:true}},
    {name:'channel',path:'/channel',component:Channel},
    {name:'search',path:'/search',component:Search},
    {name:'myCollection',path:'/myCollection',component:myCollection},
    {name:'newsDetail',path:'/newsDetail/:id',component:NewsDetail},
    {name:'myPost',path:'/myPost',component:MyPost},
    {name:'funnyComment',path:'/funnyComment/:id',component:FunnyComment}
  ]

3.5.axios部分设置 

3.5.1.axios默认基准路径 

提取请求的统一的服务器域名的部分进行抽离设置

在main.js中设置axios.defaults.baseURL

axios.defaults.baseURL = 'http://127.0.0.1:3000';

 3.5.2.axios请求拦截器

 可拦截所有请求,进行需要的配置

 这里是拦截请求,统一在请求头添加token

// 设置axios请求拦截器
axios.interceptors.request.use(config=>{
    // 判断是否有Authorization和token,如果两者都有则做用户验证
    if(!config.headers.Authorization && localStorage.getItem('token')){
        config.headers.Authorization = 'Bearer ' + localStorage.getItem('token');
    }
    return config;
})

 3.5.3.axios响应拦截器

axios.interceptors.response.use(), 可以接受一个回调函数做参数, 带有响应数据 res

拦截器拦截数据, 处理后必须放行 (return)

这里拦截所有请求的响应数据,将错误数据匹配出来弹出提示,之后每个请求可以不再单独做错误处理

// 设置响应拦截器 (统一处理响应回来的数据)
// axios.interceptors.response.use() 这个函数可以拦截到所有请求的响应,并执行逻辑
// 我们需要将逻辑函数作为参数传入
// 在入口文件,也就是组件外部,要使用 toast 需要单独引入
axios.interceptors.response.use(res=>{
    const {statusCode,message} = res.data;
    const {url} = res.config;//获取接口

    let urlArr = ['/login'];//这里的接口不走拦截器

    if(!urlArr.includes(url)){
        // 对用户信息验证失败情况进行处理
        if(statusCode && statusCode == 401 && message == '用户信息验证失败'){
            // 处理错误,在入口文件如果想要使用 vant ui 弹出窗口、
            // 这里没有 this 也没有 $toast
            // 可以使用单独引入的方式,只使用 Toast
            Toast.fail('用户信息验证失败,请重新登录');
            localStorage.removeItem('token');
            localStorage.removeItem('userId');
            router.replace('/login');
        }else if(statusCode && statusCode.toString().startsWith('4',0)){
            Toast.fail(message);
        }
    }
    return res;
  },(err)=>{
    // 暂不处理
    Toast.fail('请联系相关人员处理');
    console.log(err);
})

 3.6.css穿透scoped修改子组件样式

 lang="less" 书写less样式

scoped 在当前作用域有效 

<style lang="less" scoped></style>

 父组件想修改子组件的样式,或者想修改v-html指令中的html代码(v-html不受父组件scoped css的影响)

在需要穿透子组件的样式前端添加 /deep/ 

注意:如果是sass,/deep/可能不生效,可以使用 >>> 进行代替

/deep/ img{
        max-width: 100%;
    }

  3.7.video标签使用 

  •   video标签样式适配,一般宽度100%,高度自适应
  •  使用 controls 属性控制播放器的按钮显示
  •  使用 poster 实现图片功能(一般视频未播放前会展示一张图片), 在播放前显示图像吸引用户
  •  video常用的js事件:video.play() 播放    video.pause()暂停
<video 
   src="https://video.pearvideo.com/mp4/third/20180910/cont-1431889-10602718-163406-hd.mp4"
   poster="https://image2.pearvideo.com/cont/20180910/cont-1431889-11544080.jpg"
   width="100%"
   ref="video"
>
  您的浏览器不支持 video 标签
 </video>

 3.8.父组件中子组件标签上添加点击事件

  直接在子组件上加点击事件会不生效,这时在点击事件上带后缀.native即可生效

<AboutMe name="密码" desc="******" @click.native="showPassword = true"/>
<AboutMe name="性别" :desc="user.gender" @click.native="showGender = true"/>

  3.9.组件递归嵌套 

  效果图

  

 3.9.1.回复跟帖(直接回复第一层评论)

这里不涉及递归 

 涉及单文件组件:NewsDetail.vue(文章详情)

涉及子组件:Comment.vue(帖子),ReplyArea.vue(回复区域)

思路:帖子中包含的信息(要回复的帖子的id,帖子的用户昵称)通过点击回复传给文章详情页面,详情页面再父传子传给回复区域,回复区域调起输入框,并将用户昵称展示在输入框的placeholder中。总结:帖子(子)=(parentInfo)=》文章详情(父)==>回复区域(子)

帖子子组件中html代码:

<div class="reply" @click="callReply">回复</div>

帖子子组件中js代码:

 this.$emit('parentCallReply')触发文章详情中的parentCallReply事件

          // 这是主评论的回复事件
        // 主评论由于不参与递归,单独对待
        //以下两个方法分别都去触发文章详情的parentCallReply的事件
        callReply(){
            this.$emit('parentCallReply',{
                id:this.mainCommentItem.id,
                nickname:this.mainCommentItem.user.nickname
            })
        },

文章详情(父组件)中的html代码:

<Comment :mainCommentItem="item" v-for="item in commentList" :key="item.id" @parentCallReply="callReply"></Comment>

 文章详情(父组件)中的js代码:

 parentInfo为帖子的信息,这里通过$refs.ReplyArea调起回复区域的输入框

          callReply(parentInfo){
            // 从这里传给输入框的子组件
            this.parentInfo = parentInfo;
            // 调起输入框子组件的输入框显示事件 事件带括号
            this.$refs.ReplyArea.showTextarea();
          }

  回复区域(子组件)中的html代码:

<template>
     <div class="reply-area">
         <div class="reply-disabled" v-if="!isTrigger">
            <div class="reply">
              <input type="text" :placeholder="placeholderTxt" v-model="content"
              @focus="showTextarea">
            </div>
            <div class="other">
                <span class="count">{
   
   {commemtCount}}</span>
                <i class="iconfont iconpinglun-"></i>
                <i class="iconfont iconshoucang" :class="hasStar ? 'heightlight':''" @click="starNews"></i>
                <i class="iconfont iconfenxiang"></i>
            </div>
         </div>
         <div class="reply-abled" v-if="isTrigger">
            <div class="reply">
              <textarea rows="3" :placeholder="placeholderTxt" v-model="content"
              @blur="hideTextarea" ref="textarea"></textarea>
            </div>
            <div class="send">
                <button @click="sendReply">发送</button>
            </div>
         </div>
     </div>
</template>

  回复区域(子组件)的js代码:

 接收文章详情的传值

props:['parentInfo','hasStar','commemtCount'],

 根据parentInfo变更输入框placeholder的值

// 计算属性
    computed:{
        // 通过判断是否有昵称来变更placeholder的值
        placeholderTxt(){
            if(this.parentInfo.nickname){
                return '回复@' + this.parentInfo.nickname;
            }else{
                return '写跟贴';
            } 
        }
    },

 发送评论逻辑处理

        //自己发帖只需要传文章id和帖子内容
        sendReply(){
            this.$axios({
                // 文章id直接通过地址栏获取
                url:'/post_comment/'+this.$route.params.id,
                method:'post',
                data:{
                    content:this.content,
                    parent_id:this.parentInfo.id?this.parentInfo.id:''
                }
            }).then(res => {
                this.$emit('loadDetail');
                this.content = '';
                this.$toast(res.data.message);
            })
        },

3.9.2.回复跟帖中的评论(组件递归嵌套) 

 页面展示效果(平行):

 内部组件为递归

 

想象中的递归:

涉及单文件组件:NewsDetail.vue(文章详情)

涉及子组件:Comment.vue(帖子),ReplyArea.vue(回复区域),ParentComment.vue(递归组件,Comment.vue 的 子组件)

思路:点击ParentComment.vue(递归子组件)中的回复一层一层往外传值(parentInfo),直到传到文章详情,文章详情再传给回复区域,递归组件自己点击自己,自己给自己传值。

ParentComment.vue(递归组件)中的html代码:

点击ParentComment中的回复,调用parentCallReply方法

<template>
    <div>
        <!-- parentDepth:每一层父级都记录深度(即当前多少层),所以每一层减1 -->
        <ParentComment :commentData="commentData.parent" v-if="commentData.parent"
        :parentDepth="parentDepth" @parentCallReply="diguiCallReply"></ParentComment>
        <div class="parent-comment">
        <div class="row">
            <div class="info">
                <div class="user">
                    <img v-if="commentData.user.head_img" :src="commentData.user.head_img  | fixImgUrl" alt="">
                    <img v-else src="../../assets/logo.png" alt="">
                </div>
                <div class="other">
                    <span class="author">{
   
   {commentData.user.nickname}}</span>
                    <span class="date">{
   
   {commentData.create_date ? commentData.create_date.split('T')[0]:''}}</span>
                </div>
            </div>
            <!-- 递归的每一个父级都是这里的代码,点击回复时通过parentCallReply事件触发ParentComment组件(是父级的同时也是子级)上的parentCallReply事件将本层数据传出到上一级 -->
            <div class="reply" @click="parentCallReply">回复</div>
        </div>
        <div class="centent">{
   
   {commentData.content}}</div>
     </div>
    </div>
</template>

ParentComment.vue(递归组件)中的js代码:

parentCallReply方法触发上一层的parentCallReply方法并把本层的评论信息带上(上一层的parentCallReply依然是本层),上一层的parentCallReply方法再调起diguiCallReply方法,diguiCallReply方法再调起上上一层的parentCallReply,在点击回复的当前层上,依次向上将本层数据传出(这个过程不断自己调用自己),直到传到文章详情,后续步骤和3.9.1一致。

<script>
export default {
    // 固定写法呀, 本来父子组件是需要 import 和注册的, 但是这里的子组件就是自己, 那不能自己引入自己自己注册自己呀, 那渲染标签怎么写呢? 就是靠声明当前组件的 name 来作为标签
    name:'ParentComment',//声明当前组件的 name 来作为标签来使用
    props:['commentData','parentDepth'],
    methods:{
        parentCallReply(){
            // 每一层都有触发自定义事件parentCallReply
            // 并将父级本层id和昵称传出留作他用
            this.$emit('parentCallReply',{
                id:this.commentData.id,
                nickname:this.commentData.user.nickname
            })
        },
        // 因为是递归父级,所以在当前父级的上一级就要使用parentCallReply事件
        diguiCallReply(parentInfo){
            this.$emit('parentCallReply',parentInfo)
        }
    }
}
</script>

  注意:本组件内使用自己的组件,通过声明本组件的name值作为标签来做本组件中使用

name:'ParentComment',//声明当前组件的 name 来作为标签来使用

  3.10.vantUI使用(非按需引入)

  传送门:https://youzan.github.io/vant/#/zh-CN/

  3.10.1.vantUI引入 

# 通过 npm 安装
npm i vant -S

  在main.js中引入全部组件

// 1.导入组件库
import Vant from 'vant';
// 2.引入对应的css文件
import 'vant/lib/index.css';
// 3.注册组件
Vue.use(Vant);

  3.10.2.Toast(轻提示)

//没有图标样式,纯提示
this.$toast(message);
//成功提示
this.$toast.success(message);
//失败提示
this.$toast.fail(message);

  3.10.3.Dialog(弹出框)和field(输入框组件)

         <van-dialog 
         v-model="showNickName" 
         title="编辑昵称" 
         show-cancel-button
         confirmButtonColor="pink"
         cancelButtonColor="#c0c0c0"
         @confirm="editUserInfo({'nickname':nickName})"	
         >
            <van-field v-model="nickName" placeholder="请输入昵称"/>
         </van-dialog>

  3.10.4.ActionSheet(动作面板)

<!-- 编辑性别 -->
<van-action-sheet v-model="showGender" :actions="actions" @select="onSelect" cancel-text="取消" close-on-click-action/>
actions:[
               { name: '男', gender:'1'}, 
               { name: '女',gender:'0' }
           ]

3.10.5.Uploader(文件上传)

v-bind 形式传入一个 :after-read

在 methods 当中定义这个函数, 就是处理图片上传逻辑的函数

这个函数自动接收一个参数 fileObj

使用 ajax + FormData 对象上传图片即可  

<van-uploader :after-read="uploadImg" />
       uploadImg(fileObj){
            // 会接受一个参数 fileObj 文件对象
            // 其中文件就在 fileObj.file
            // 先将图片转成二进制的参数格式
            let formdata = new FormData();
            formdata.append('file',fileObj.file);
            this.$axios({
                url:'/upload',
                method:'post',
                data:formdata
            }).then(res=>{
                if(res.data.message == '文件上传成功'){
                   this.editUserInfo({'head_img':res.data.data.url});
                }
            })
            
        }

  

3.10.6.Tab(标签页)/ List(列表)

标签页 v-model 是当前激活的栏目索引

             <van-tabs v-model="active" title-active-color="pink" color="pink" sticky animated swipeable>
                <van-tab :title="category.name" v-for="category in categoryList" :key="category.id">
                    <!-- 使用 list 组件将文章列表包裹起来
                    每当 list 被拉到第, 就会触发一个事件 load  -->
                    <!-- v-model 声明了这个列表是否正在加载中
                    如果为 false 那么拉到底就会发出下一页的请求
                    自动变为 true 然后等待,不会重复发送 -->
                    <!-- finished 属性标注当前列表是否已经到底, 默认是 false
                    如果是 true , 那么就显示借宿文字, 再也不会发送下一页请求了 -->
                    <!-- 这个组件每当拉倒第就会触发 load 事件, 可以监听他, 发送下一页请求 -->
                    <van-list
                    v-model="category.loading"
                    :finished="category.finished"
                    finished-text="没有更多了"
                    :immediate-check="false"
                    @load="loadMore"
                    >
                    <NewsItem :postData="item" v-for="item in category.postList" :key="item.id"></NewsItem>
                    </van-list>
                </van-tab>
            </van-tabs>

 List列表实现无限加载效果(分页)

该方法获取分类,获取数据后将每一个分类的所有文章数据(空数组,尚未去获取)初始化好,并添加每一个分类的文章要使用的分页

getCategory(){
            this.$axios({
                url:'/category',
                methods:'get'
            }).then(res=>{
                // 这里是为了在获取栏目造好数据结构,以便方便文章数据存储在里面,这样再次点击栏目时可以从
                // 该数据结构中直接获取文章列表,而不需要再次发送请求获取
                const newData = res.data.data.map(category => {
                    // 将原返回数据中对象与其他数据封装成一个对象返回
                    return{
                        ...category,
                        postList:[],
                        pageIndex:1,
                        pageSize:5,
                        // 是否正在加载
                        loading:false,
                        // 是否已经全部加载完毕
                        finished:false
                    } 
                });
                this.categoryList = newData;
                if(this.categoryList.length > 0){
                    // 打开页面时获取热点文章数据
                    this.getPost();
                }
            })
        },

 获取文章

getPost(){
            const currentCategoryList = this.categoryList[this.active];
            this.$axios({
                url:'/post',
                method:'get',
                params:{
                    category: currentCategoryList.id,
                    pageIndex: currentCategoryList.pageIndex,
                    pageSize: currentCategoryList.pageSize
                }
            }).then(res=>{
                // 获取文章后, 不应该放入公共 data 中的 postList
                // 而是放入当前激活栏目 的 postList
                // 以前我们获取文章列表之后, 直接将之前的数组替换掉
                // 其实我们需要的是, 下一页的内容跟上一页拼接起来
                // 展开,合并
                this.categoryList[this.active].postList = [...currentCategoryList.postList,...res.data.data];
                // 这里加载完了文章列表数据, 然后需要手动将当前栏目的加载状态改回 false 也就是没有正在加载
                // 这样子才能在下次拉到底的时候重新触发加载下一页
                currentCategoryList.loading = false;
                // 如果这一页数据的数量少于页容量,则加载结束,数据已全部加载完
                if(res.data.data.length < currentCategoryList.pageSize){
                    currentCategoryList.finished = true;
                }
            })
        },
       loadMore(){
            // 读取更多文章, 实际上
            // 就是将当前栏目的 pageIndex 加一
            // 发送文章获取请求即可
            console.log('加载下一页');
            this.categoryList[this.active].pageIndex +=1;
            this.getPost();
        },

 监听分类的切换,并判断有没有获取过文章

watch:{
        active(){
            // 如果这个栏目之前没有获取过文章列表,那么此时就去获取一次
            if(this.categoryList[this.active].postList.length == 0){
                this.getPost();
            } 
        }
    },

猜你喜欢

转载自blog.csdn.net/THcoding_Cat/article/details/107093068