头条移动端笔记

这篇博客详细记录了移动端开发中的若干关键点,包括rem适配、路由配置、axios封装、本地存储优化、列表加载控制、下拉刷新、时间处理、频道管理、搜索功能、数据持久化以及组件交互等实战技巧。
摘要由CSDN通过智能技术生成

移动端 rem 适配:

安装:

yarn add amfe-flexible

在 main.js 中加载执行该模块:

import 'amfe-flexible'

 安装: 

npm install postcss-pxtorem@5.1.1  -D 

项目根目录中创建 .postcssrc.js 或 postcss.config.js 文件 

配置只适用于vant内部的相关组件内容,我们自己书写的样式,并不是按照这个,我们希望设计图是多少px,就写多少px,故而修改内容如下:

module.exports = {
  plugins: {
    // postcss-pxtorem 插件的版本需要 >= 5.0.0
    'postcss-pxtorem': {
      rootValue({ file }) {  // 如果是vant的就按照375 尺寸, 如果是其他的就是按照750
        return file.indexOf('vant') !== -1 ? 37.5 : 75;   // rootValue 的值一般是 设计稿  1/10
      },
      propList: ['*'],
    },
  },
};

 配置路由:

src/router/index.js 中:

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 路由映射数组
const routes = [
  {
    path: '/login',
    name: 'login',
    component: () => import('@/views/login') // 路由的懒加载
  }
]
// 实例化路由对象
const router = new VueRouter({
  routes
})

export default router // 暴露出去

封装axios请求:

创建 src/utils/request.js:

/**
 * 封装 axios 请求模块
 */
import axios from 'axios'

// 创建一个axios实例对象
const request = axios.create({
  baseURL: 'http://toutiao.itheima.net' // 基础路径
})

// 配置请求拦截器

// 配置响应拦截器

// 暴露出去
export default request

创建 src/api/user.js:

请求登录:

import request from '@/utils/request'

// 实现登录的接口
export const login = data => {
  return request({
    method: 'POST',
    url: '/v1_0/authorizations',
    data
  })
}

优化封装本地存储操作模块:

创建 src/utils/storage.js 模块:

// 封装优化处理token

// 存储数据
export const setItem = (key, data) => {
    if (typeof data === 'object') {
        data = JSON.stringify(data)
    }
    window.localStorage.setItem(key, data)
}

// 获取本地存储的数据
export const getItem = (key) => {
    const data = window.localStorage.getItem(key)
    try {
        return JSON.parse(data)
    } catch (err) {
        return data
    }
}
// 删除数据
export const removeItem = (key) => {
    window.localStorage.removeItem(key)
}

使用的时候可以直接:

import { setItem, getItem } from '@/utils/storage'

state: {
    // 存储当前登录的用户信息(token等数据)
    // user: JSON.parse(window.localStorage.getItem(TOKEN_KEY))
    user: getItem(TOKEN_KEY)
  },

 mutations: {
    setUser(state, data) {
      state.user = data
      // 本地存储, 防止刷新丢失
      // window.localStorage.setItem(TOKEN_KEY, JSON.stringify(state.user))
      setItem(TOKEN_KEY, state.user)
    }
  },

优化设置 Token, 配置请求拦截器:

在 src/utils/request.js 中添加拦截器统一设置 token:

import store from '@/store'

// 配置请求拦截器
request.interceptors.request.use(function (config) {
  // config :本次请求的配置对象
  // config 里面有一个属性:headers
  const user = store.state.user
  if (user && user.token) {
    config.headers.Authorization = `Bearer ${user.token}`
  }
  return config
}, function (error) {
  return Promise.reject(error)
})

 请求数据使用的时候: 就可以不用写请求头了

// 获取个人用户信息
export const getUserInfo = () => {
  return request({
    method: 'GET',
    url: '/v1_0/user',
    // headers: {
    //   Authorization: `Bearer ${store.state.user.token}`
    // }
  })
}

点击返回上一页:  @click="$router.back()"

点击返回指定页面路径:  @click="$router.push('/login')"

在CSS里面使用@路径的表达: src="~@/assets/mobile.png"

插槽的两个使用方式:

  <template #left-icon>
          <i class="toutiao toutiao-shouji"></i>
   </template>

// 一样的

  <i slot="icon" class="toutiao toutiao-lishi"></i>

展示频道列表:

思路:

  • 第1步:api目录下面文件封装请求方法
  • 第2步:data里面定义变量用于存储数据
  • 第3步:methods里定义获取数据方法
  • 3.1 页面里面导入这个方法
  • 3.2 methods方法里面调用方法发送请求
  • 3.3 请求错误处理
  • 3.4 请求成功赋值data里面变量
  • 第4步:created里面调用这个方法
  • 第5步: 渲染数据

解决滚动条位置问题: 不设置高度的话, 会共用body的高度, 实现不了滚动条记住位置的效果, 所以要给大盒子一个高度.article-list:

<style lang='less' scoped>
.article-list {
  // 百分比单位是相对于父元素的
  // height: 100%;

  // 视口(在移动端是布局视口)单位:vw 和 vh,不受父元素影响
  // 1vw = 视口宽度的百分之一
  // 1vh = 视口高度的百分之一
  height: 79vh;
  overflow-y: auto; // 滚动条
}
</style>

List 组件通过 loading 和 finished 两个变量控制加载状态, 当组件初始化或滚动到到底部时,会触发 load 事件并将 loading 设置成 true,此时可以发起异步操作并更新数据,数据更新完毕后,将 loading 设置成 false 即可。 若数据已全部加载完毕,则直接将 finished 设置成 true 即可。

  • load 事件
    • List 初始化后会触发一次 load 事件,用于加载第一屏的数据。
    • 如果一次请求加载的数据条数较少,导致列表内容无法铺满当前屏幕,List 会继续触发 load 事件,直到内容铺满屏幕或数据全部加载完成。
  • loading 属性:控制加载中的 loading 状态
    • 非加载中,loading 为 false,此时会根据列表滚动位置判断是否触发 load 事件(列表内容不足一屏幕时,会直接触发)
    • 加载中,loading 为 true,表示正在发送异步请求,此时不会触发 load 事件
  • finished 属性:控制加载结束的状态
    • 在每次请求完毕后,需要手动将 loading 设置为 false,表示本次加载结束
    • 所有数据加载结束,finished 为 true,此时不会触发 load 事件

  try {
        // 1. 发送请求获取数据
        const { data } = await getArticles({
          channel_id: this.channel.id,
          timestamp: this.pre_timestamp || Date.now(), // 时间戳,请求新的推荐数据传当前的时间戳,请求历史推荐传指定的时间戳
          with_top: 1,
        });
        // if (Math.random() > 0.5) {
        //   throw new Error("错误");
        // } 错误的测试代码
        console.log(data);
        // 2. 将数据追加到data 数组中保存
        const { results } = data.data;
        this.list = [...this.list, ...results];
        // 3. 将loading设置false
        this.loading = false;
        // 将时间戳更新实现翻页请求历史数据
        this.pre_timestamp = data.data.pre_timestamp;
        // 4. 判断数据是否加载完成
        if (data.data.pre_timestamp === null) {
          // 数据已经加载完成
          this.finished = true;
        }
      } catch (error) {
        this.loading = false;
        this.error = true;
        this.$toast("获取当前频道的文章失败!");
      }
    },

下拉刷新:  isLoading: false, // 下拉刷新加载状态  使用van-pull-refresh组件

处理相对时间:

安装: 

npm i dayjs
// 导入语言包
import 'dayjs/locale/zh-cn'
// 导入相对时间的插件
import relativeTime from 'dayjs/plugin/relativeTime'
// 使用中文语言包
dayjs.locale('zh-cn')

// console.log(dayjs().format('YY-MM-DD'));

// 注册相对时间的插件
dayjs.extend(relativeTime)
// 定义全局格式时间的过滤器
Vue.filter('relativeTime', time => {
    return dayjs().to(dayjs(time))
})

 使用的时候用 |

展示推荐频道列表:

所有频道列表 - 我的频道 = 剩余推荐的频道

实现过程所以一共分为两大步:

  • 获取所有频道
  • 基于所有频道和我的频道计算获取剩余的推荐频道

方法一:

  computed: {
    // 推荐频道=所有-我的
    recommentChannels() {
      let arr = []; // 推荐频道
      this.allChannels.forEach((channel) => {
        let ret = this.myChannels.find((myChannel) => {
          return myChannel.id === channel.id;
        });
        // ret true 用户频道
        // ret false 推荐频道
        if (!ret) {
          arr.push(channel);
        }
      });
      return arr;
}
}

 方法二:

  computed: {

    return this.allChannels.filter((channel) => {
      return !this.myChannels.find((myChannel) => {
          return myChannel.id === channel.id;
          });
      });

}

然后模板绑定:

 <van-grid-item
                   class="grid-item"
                   v-for="(channel, index) in recommendChannels"
                   :key="index"
                   icon="plus"
                   :text="channel.name"
                   />
</van-grid>

添加频道:

思路:

  • 给推荐频道列表中每一项注册点击事件
  • 获取点击的频道项
  • 将频道项添加到我的频道中
  • 将当前点击的频道项从推荐频道中移除
    • 不需要删除,因为我们获取数据使用的是计算属性,当我频道发生改变,计算属性重新求值了

编辑频道:

思路:

  • 给我的频道中的频道项注册点击事件
  • 在事件处理函数中
    • 如果是编辑状态,则执行删除频道操作
    • 如果是非编辑状态,则执行切换频道操作
  // 点击添加
    anAddChannel(channel) {
      this.myChannels.push(channel);
    },
    onMyChannelClick(channel, index) {
      // 判断是否为编辑状态
      if (this.isEdit) {
        // 编辑删除
        // 不能删除推荐频道
        if (this.fiexdChannels.includes(channel.id)) {
          return;
        }
        if (index < this.active) {
          this.$emit("updateActive", this.active - 1, true);
        }
        this.myChannels.splice(index, 1);
      } else {
        // 非编辑切换频道
        this.$emit("updateActive", index, false);
      }
    },

固定推荐频道不能删除, 没删除按钮:

首先在data里声明 fiexdChannels: [0], // 固定推荐频道, 不允许删除

给删除按钮绑定是否出现的时候: 用includes取反

<van-icon
          slot="icon"
          name="clear"
          v-show="isEdit && !fiexdChannels.includes(index)"
        ></van-icon>

数据持久化:

思路:

  • 如果未登录,则存储到本地
  • 如果已登录,则存储到线上
    • 找到数据接口
    • 封装请求方法
    • 请求调用

搜索页面:

三个页面的v-if  v-else-if  v-else的使用:


    <!-- 搜索结果 -->
    <search-result v-if="isResultShow" :search-text="searchText"/>
    <!-- /搜索结果 -->

    <!-- 联想建议 -->
    <search-suggestion
      v-else-if="searchText"
      :search-text="searchText"
      @search="onSearch"
    />
    <!-- /联想建议 -->

    <!-- 搜索历史记录 -->
    <search-history v-else />
    <!-- /搜索历史记录 -->

// isResultShow: false, // 控制搜索结果列表的显示和隐藏

 防抖的实现:

安装 lodash:

 yarn add lodash
// lodash 支持按需加载,有利于打包结果优化
import { debounce } from "lodash"
  watch: {
    // 监听搜索关键词的变化
    searchText: {
      //   handler 只有监听的数据发生变化的时候执行
      // 防抖: debounce(需要防抖的函数function,延迟时间:毫秒)
      handler: debounce(function (val) {
        console.log(val);
        this.loadSearchSuggestions(val);
      }, 200),
      // 所以设置immediate: 页面初始化一打开就立即执行
      immediate: true,
    },
  },

 关键词高亮的实现:

  highlight(text) {
      const highlightStr = `<span style="color:red;">${this.searchText}</span>`;
      // RegExp 正则表达式构造函数 g全局 i忽略大小写
      // new RegExp(要匹配的模板字符串, 匹配的模式)
      const reg = new RegExp(this.searchText, "gi");
      // replace替换 把text中的符合reg正则规则的 替换成 highlightStr
      // replace(正则规则,要替换成的)
      return text.replace(reg, highlightStr);
    },

 然后用插槽渲染到页面:

 <div slot="title" v-html="highlight(text)"></div

搜索结果页面的实现:

 data() {
    return {
      list: [],
      loading: false,
      finished: false,
      page: 1, // 页数,默认为1
      per_page: 10, // 每页数量
    };
  },
 methods: {
    async onLoad() {
      // 1. 获取数据
      try {
        const { data } = await getSearchResult({
          page: this.page,
          per_page: this.per_page,
          q: this.searchText,
        });
        console.log(data);
        const { results } = data.data;
        // 2. 将获取到的数据追加到数组, 实现数据的更新
        this.list = [...this.list, ...results];
        // 3. 将loading的状态设置为false
        this.loading = false;
        // 4. 判断数据是否加载完毕, 如果加载完毕 将finished设置为true
        if (results.length) {
          // 实现翻页 page加1
          this.page++;
        } else {
          // 数据请求完毕
          this.finished = true;
        }
      } catch (error) {
        this.loading = false;
        this.$toast("获取搜索结果数据失败!");
      }
    },
  },

搜索历史记录

在data中保存搜索历史记录: searchHistories,

定义方法: 
 methods: {
   onSearch(val) {
      this.searchText = val;

      // 不能加入重复的数据, 所以添加之前需要判断, 如果重复就先删除再添加
      const index = this.searchHistories.indexOf(val);
      if (index !== -1) {
        this.searchHistories.splice(index, 1);
      }
      // 向后添加历史记录
      this.searchHistories.unshift(val);

      this.isResultShow = true;
      console.log(val);
    },
  },

删除历史记录:

在data中保存是否删除的状态: isDeleteShow: false,

定义方法:
  methods: {
    // 删除
    onSearchItemClick(index, text) {
      // 判断是否处于删除的状态
      if (this.isDeleteShow) {
        // 是, 删除数据
        this.searchHistories.splice(index, 1);
      } else {
        // 不是, 实现搜索
        this.$emit("search", text);
      }
    },
  },

实现全部删除: 子传父

在父组件中, 定义自定义事件: 

@clearHistories="searchHistories = []"

 在子组件中, 删除按钮绑定事件, 触发自定义事件:

<span @click="$emit('clearHistories')">全部删除 </span>

数据持久化:

// 导入本地存储封装方法
import { setItem, getItem } from '@/utils/storage'

 设置监听:

  watch: {
    searchHistories(val) {
      // 往本地存储同步搜索的记录
      console.log(val);
      // 打印的是字符串, 没有必要深度监听, 数组对象才用深度监听
      setItem("HISTORIES-LIST", val);
    },
  },

 然后初始化的时候从本地存储获取数据:

searchHistories: getItem("HISTORIES-LIST") || [],

文章详情实现:

每篇文章都有自己的Id号, 发送请求的时候要带上Id

/**
* 根据 id 获取指定文章
*/
export const getArticleById = articleId => {
  return request({
    method: 'GET',
    url: `/v1_0/articles/${articleId}`
  })
}

 配置路由的时候使用 动态路径参数的解耦:

  {
    path: '/article/:articleId',
    name: 'Article',
    component: () => import('@/views/article'),
    props: true // 动态路径参数的解耦: 将动态路径的参数映射到对应组件的props属性中 这里就是articleId
  }

 然后在该页面views/article/index.vue中:

props: {
    // 使用props获取动态路由的数据
    articleId: {
      type: [Number, String],
      required: true,
    },
  },

 在src/components/article-item/index.vue配置路由跳转:

 <!-- 配置路由跳转: 
    @click="$router.push('/article/' + article.art_id)"
    :to="'/article/' + article.art_id"
    :to="`/article/${article.art_id}`"
    :to="{ name:'路径名称', params:{ 标识符:数据 } } -->
  <van-cell
    class="article-item"
    :to="{ name: 'Article', params: { articleId: article.art_id } }"
  >

四个页面设置

隐藏显现:

<!-- 加载中 -->
<div v-if="loading" class="loading-wrap"> ...  </div>
<!-- /加载中 -->

<!-- 加载完成-文章详情 -->
<div v-else-if="article.title" class="article-detail"> ... </div>
<!-- /加载完成-文章详情 -->

<!-- 加载失败:404 -->
<div v-else-if="errStatus === 404" class="error-wrap"> ... </div>
<!-- /加载失败:404 -->

<!-- 加载失败:其它未知错误(例如网络原因或服务端异常) -->
<div v-else class="error-wrap">...</div>
 <!-- /加载失败:其它未知错误(例如网络原因或服务端异常) -->

处理正文样式:

 下载github-markdown-css

导入

<style scoped lang="less">
@import "./github-markdown.css"; 

 然后添加类名: markdown-body

.postcssrc.js中配置不要转换样式文件中的字号:

   // 配置不要转换的样式资源
      exclude: 'github-markdown'  // 增加这一句!

实现图片点击预览:

思路:

1、从文章内容中获取到所有的 img DOM 节点

2、获取文章内容中所有的图片地址

3、遍历所有 img 节点,给每个节点注册点击事件

4、在 img 点击事件处理函数中,调用 ImagePreview 预览

 <!-- 增加ref属性 -->
 <div class="article-content markdown-body"   ref="article-content" v-html="article.content" ></div>
 data() {
    return {
      article: {}, // 2.定义变量存储文章详情
      loading: true, // 加载中的 loading 状态
      errStatus: 0, // 失败的状态码
    };
  },
 created() {
    this.loadArticle();
  },
  mounted() {},
  methods: {
    async loadArticle() {
      // 展示 loading 加载中
      this.loading = true;
      try {
        const { data } = await getArticleById(this.articleId);
        // console.log(data);
        data.data.content = data.data.content.replaceAll(
          "https://images.weserv.nl/?url=",
          ""
        );
        this.article = data.data; // vue的dom更新是异步的  微任务
        // console.log(this.article);

        // 宏任务
        setTimeout(() => {
          // 在数据更新之后, 调用图片预览功能
          this.previewImage();
        }, 0);
      } catch (error) {
        if (error.response && error.response.status === 404) {
          this.errStatus = 404;
        } else {
          this.$toast("获取文章详情失败!");
        }
      }
      // 无论成功还是失败,都需要关闭 loading
      this.loading = false;
    },
    // 处理文章中的图片预览功能
    previewImage() {
      // 1. 从文章内容中获取到所有的img DOM节点
      const articleContent = this.$refs["article-content"]; // 获取到了容器节点
      const imgs = articleContent.querySelectorAll("img");
      // 2. 获取文章内容中所有的图片地址
      const images = [];
      imgs.forEach((img, index) => {
        images.push(img.src);
        // 3. 遍历所有img节点, 给每个节点注册点击事件
        // 4. 在img点击事件处理函数中, 调用ImagePreview预览
        img.onclick = () => {
          ImagePreview({
            images: images,
            // 指定预览图片的起始位置
            startPosition: index,
          });
        };
      });
    },
  },

组件的v-model的使用:

在父组件: 

    <!-- 关注 -->
          <!-- $event是用来接受子组件传过来的值 -->
          <!-- 在使用组件的同一个变量既要传递数据又需要修改数据
          传递prpps:
          :is-followed="article.is_followed"
          修改: 自定义事件
           @updateFollow="article.is_followed = $event"
           -->
          <!-- 简化: -->
          <!-- 可以使用简化的方式: v-model -->
          <!-- 传递props:
           :value="article.is_followed"
           修改: 内置封装 input
           @input="article.is_followed = $event"
            -->

          <FollowUser
            :follow-userid="article.aut_id"
            v-model="article.is_followed"
            class="follow-btn"
          ></FollowUser>

 在子组件:

 props: {
    value: {
      type: Boolean,
      required: true,
    },
    followUserid: {
      type: [String, Number],
      required: true,
    },
  },


子传父的:
 this.$emit("input", !this.value);

 传入props的是value,自定义事件是input, 

	总结: v-model  是 value属性和input事件的语法糖!

还可以自定义修改value和input名字

  // module: {

  //   prop: "userFollowed", // 修改prop属性名

  //   event: "changeFollowed", // 修改事件名

  // },

实现关注用户的时候要更新视图, 因为发送请求更新的只是后台数据 article是我们自己加的 刷新才可以 所有不刷新的话 我们要手动更新视图的状态 取反

关注用户的实现:

 methods: {
    // 关注用户和取消关注功能
    async onFollow() {
      this.followLoading = true;
      // 判断用户是否登录, 如果没有请先登录
      if (!this.$store.state.user) return this.$toast("未登录, 请先登录!");
      // 发送请求实现添加关注和取消关注
      try {
        const AutId = this.followUserid;
        // 判断是否已经关注
        if (this.value) {
          // 已经关注了就取消关注 发送请求
          await deleteFollow(this.followUserid);
        } else {
          // 没有关注就发送关注请求
          await addFollow(this.followUserid);
        }
        // 更新视图
        // 发送请求更新的只是后台数据 article是我们自己加的 刷新才可以 所有不刷新的话 我们要手动更新视图的状态 取反
        // this.isFollowed = !this.isFollowed;
        // 子传父
        // this.$emit("updateFollow", !this.isFollowed);
        this.$emit("input", !this.value);
      } catch (error) {
        if (error && error.response.status === 400) {
          this.$toast("不能关注自己!");
        } else {
          this.$toast.fail("操作失败!");
        }
      }
      this.followLoading = false;
    },
  },

祖先后代通信:

// 给所有的后代组件提供数据
provide: function () {
    return {
        articleId: this.articleId  
    }
}

 comment-post.vue 注入数据:

// inject:['articleId']
inject: {
    articleId: {
        type: [Number, String, Object],
        default: null
    }
}

两种写法

 

整个文章详情的思路:

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值