07 -全局状态管理

news2024/11/15 21:44:56

全局状态管理

7-1:开篇

在上一章中我们完成了 “一半” 的文章搜索功能,并且留下了一些问题。那么这些历史残留的问题,我们将会在本章节中通过 全局状态管理工具 进行处理。

那么究竟什么是 全局状态管理工具如何在 uniapp 中使用 全局状态管理工具,剩下的 文章搜索功能 搜索功能如何完成,我们是否还会再遇到其他的坑呢?

这些内容我们都会在本章节中为大家一一讲解。

7-2:状态管理 - 全局状态管理工具

场景

在上一章中,我们遇到了一个问题:search-blogsearch-historysearchData 和 组件 之间 强耦合

如果想要解决这个问题,那么我们需要使用到一个叫做:全局状态管理工具 的东西。

学习过 vue 的同学应该知道,在 vue 中,存在一个 vuex 的库,这个库的作用就是:全局状态管理

而在 uniapp 里,如果我们想要实现 全局状态管理,那么 vuex 也将是一个非常好的选择。

那么下面我们就来看一下,什么是 全局状态管理工具,以及什么是 vuex

问题

  1. 什么是全局状态管理模式 和 全局状态管理工具
  2. 什么是 vuex

内容

“单向数据流” 理念示意图
请添加图片描述

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

但是,当我们的应用遇到**多个组件共享状态(数据)**时,单向数据流的简洁性很容易被破坏(回忆 search-blogsearch-history 的代码):

  • 多个视图依赖于同一状态(数据)。
  • 来自不同视图的行为需要变更同一状态(数据)。

所以我们不得不通过 父子组件传递数据 的方式,来频繁的修改状态(数据)。但是这种方式是 非常脆弱,通常会导致无法维护的代码。


什么是全局状态管理模式

所以我们就需要就想到了一种方案,我们把:

把多个组件之间共用的数据抽离出来,通过一个 单例模式 进行管理,而这种管理的方式就叫做【全局状态管理模式】。

具备 【全局状态管理模式】 的库,就是 【全局状态管理工具】

而在 vue 中存在一个专门的 【全局状态管理工具】,叫做 vuex

因为 uniapp 追随 vue微信小程序 的语法,所以我们可以在 uniapp 中使用 vuex 来进行 【全局状态管理】,并且这是一个 非常被推荐 的选择。

答案

  1. 什么是全局状态管理模式 和 全局状态管理工具
    1. 模式:把多个组件之间共用的数据抽离出来,通过一个 单例模式 进行管理。
    2. 工具:具备 【全局状态管理模式】 的库
  2. 什么是 vuex
    1. vue 应用程序进行全局状态管理的工具

7-3:状态管理 - 在项目中导入 vuex

**创建 store **

// 1. 导入 Vue 和 Vuex
import Vue from 'vue';
// uniapp 已默认安装,不需要重新下载
import Vuex from 'vuex';

// 2. 安装 Vuex 插件
Vue.use(Vuex);
// 3. 创建 store 实例
const store = new Vuex.Store({});
export default store;

在 main.js 中注册 vuex 插件


// 导入 vuex 实例
import store from './store';
...

const app = new Vue({
  ...App,
  store // 挂载实例对象
});
app.$mount();

7-4:状态管理 - 测试 vuex 是否导入成功

**创建 modules/search.js **

export default {
  // 独立命名空间
  namespaced: true,
  // 通过 state 声明数据
  state: () => {
    return {
      msg: 'hello vuex'
    };
  }
};

index.js 中注入模块

...
// 导入 search.js 暴露的对象
import search from './modules/search';

// 2. 安装 Vuex 插件
Vue.use(Vuex);
// 3. 创建 store 实例
const store = new Vuex.Store({
  modules: {
    search
  }
});
export default store;

search-blog 中使用 模块中的数据

<template>
  <view class="search-blog-container">
    <!-- 3. 使用导入的 vuex 模块中的数据 -->
    <div>{{ msg }}</div>
    ...
  </view>
</template>

<script>
// 1. 导入 mapState 函数
import { mapState } from 'vuex';
...
export default {
 ...
  computed: {
    // 2. 在 computed 中,通过 mapState 函数,注册 state 中的数据,导入之后的数据可直接使用(就像使用 data 中的数据一样)
    // mapState(模块名, ['字段名','字段名','字段名'])
    ...mapState('search', ['msg'])
  }
};
</script>


7-5:状态管理 - 构建 search 模块

search.js

export default {
  // 独立命名空间
  namespaced: true,
  // 通过 state 声明数据
  state: () => ({
    searchData: []
  }),
  // 更改 state 数据的唯一方式是:提交 mutations
  mutations: {
    /**
     * 添加数据
     */
    addSearchData(state, val) {
      if (!val) return;
      const index = state.searchData.findIndex((item) => item === val);
      if (index !== -1) {
        state.searchData.splice(index, 1);
      }
      state.searchData.unshift(val);
    },
    /**
     * 删除指定数据
     */
    removeSearchData(state, index) {
      state.searchData.splice(index, 1);
    },
    /**
     * 删除所有数据
     */
    removeAllSearchData(state) {
      state.searchData = [];
    }
  }
};

7-6:状态管理 - 使用 search 模块完成搜索历史管理

search-blog

<template>
  <view class="search-blog-container">
...
    <!-- 热搜列表 -->
    <view class="search-hot-list-box card" v-if="showType === HOT_LIST">
      <!-- 列表 -->
      <search-hot-list @onSearch="onSearchConfirm" />
    </view>
...
  </view>
</template>

<script>
// 导入 mapMutations 函数
import { mapMutations } from 'vuex';
...
export default {
  ...
  methods: {
    ...mapMutations('search', ['addSearchData']),
    /**
     * 搜索内容
     */
    onSearchConfirm(val) {
      ...
      // 保存搜索历史数据
      this.addSearchData(this.searchVal);
      ...
    },
    /**
     * @deprecated
     * 保存搜索历史数据
     */
    saveSearchData() {
      // 1. 如果数据已存在,则删除
      const index = this.searchData.findIndex((item) => item === this.searchVal);
      if (index !== -1) {
        this.searchData.splice(index, 1);
      }
      // 2. 新的搜索内容需要先于旧的搜索内容展示
      this.searchData.unshift(this.searchVal);
    },
    /**
     * @deprecated
     * 删除数据
     */
    onRemoveSearchData(index) {
      this.searchData.splice(index, 1);
    },
    
  }
};
</script>


search-history

<script>
// 1. 导入 mapState 函数
import { mapState, mapMutations } from 'vuex';

export default {
  name: 'search-history',
  props: {
    /**
     * @deprecated
     */
    // searchData: {
    //   type: Array,
    //   required: true
    // }
  },

  methods: {
    ...mapMutations('search', ['removeSearchData', 'removeAllSearchData']),
    onClearAll() {
      uni.showModal({
        title: '提示',
        content: '删除搜索历史记录?',
        showCancel: true,
        success: ({ confirm, cancel }) => {
          if (confirm) {
            // 删除 searchData
            this.removeAllSearchData();
            // 返回状态
            this.isShowClear = false;
          }
        }
      });
    },
    onHistoryItemClick(item, index) {
      if (this.isShowClear) {
        // 删除指定的 searchData
        this.removeSearchData(index);
      } else {
        this.$emit('onItemClick', item);
      }
    }
  },
  computed: {
    // 2. 在 computed 中,通过 mapState 函数,注册 state 中的数据,导入之后的数据可直接使用(就像使用 data 中的数据一样)
    // mapState(模块名, ['字段名','字段名','字段名'])
    ...mapState('search', ['searchData'])
  }
};
</script>

7-7:状态管理 - 数据持久化

已完成 数据和组件的分离,所以【数据持久化】不需要涉及到组件内的代码

store/search.js

const STORAGE_KEY = 'search-list';

export default {
  // 独立命名空间
  namespaced: true,
  // 通过 state 声明数据
  state: () => ({
    // 优先从 storage 中读取
    searchData: uni.getStorageSync(STORAGE_KEY) || []
  }),
  // 更改 state 数据的唯一方式是:提交 mutations
  mutations: {
    /**
     * 保存数据到 storage
     */
    saveToStorage(state) {
      uni.setStorage({
        key: STORAGE_KEY,
        data: state.searchData
      });
    },
    /**
     * 添加数据
     */
    addSearchData(state, val) {
      ...
      // 调用 saveToStorage
      this.commit('search/saveToStorage');
    },
    /**
     * 删除指定数据
     */
    removeSearchData(state, index) {
      ...
      // 调用 saveToStorage
      this.commit('search/saveToStorage');
    },
    /**
     * 删除所有数据
     */
    removeAllSearchData(state) {
      ...
      // 调用 saveToStorage
      this.commit('search/saveToStorage');
    }
  }
};

7-8:搜索结果 - 获取搜索结果数据

search.js

import request from '../utils/request';

/**
 * 搜索结果
 */
export function getSearchResult(data) {
  return request({
    url: '/search',
    data
  });
}

search-result-list.vue

<template>
  <view> 搜索结果 </view>
</template>

<script>
import { getSearchResult } from 'api/search';
export default {
  name: 'search-result-list',
  props: {
    // 搜索关键字
    queryStr: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      // 数据源
      resultList: [],
      // 页数
      page: 1
    };
  },
  created() {
    this.loadSearchResult();
  },
  methods: {
    /**
     * 获取搜索数据
     */
    async loadSearchResult() {
      const { data: res } = await getSearchResult({
        q: this.queryStr,
        p: this.page
      });
      this.resultList = res.list;
      console.log(this.resultList);
    }
  }
};
</script>

<style lang="scss"></style>

search-blog

    <!-- 搜索结果 -->
    <view class="search-result-box" v-else>
      <search-result-list :queryStr="searchVal" />
    </view>

7-9:搜索结果 - 渲染搜索结果数据

创建三个组件,应该应对三种展示情况:

  1. search-result-item-theme-1:无图片样式
  2. search-result-item-theme-2:一张图片样式
  3. search-result-item-theme-3:三张图片样式

search-result-item-theme-1

<template>
  <view class="search-result-item-box">
    <!-- 标题 -->
    <rich-text :nodes="data.title" class="item-title line-clamp-2"></rich-text>
    <!-- 内容区 - 样式 1 -->
    <rich-text :nodes="data.description" class="item-desc line-clamp-2"></rich-text>
    <!-- 底部 -->
    <view class="item-desc-bottom">
      <view class="item-author">{{ data.author }}</view>
      <view class="item-read-num">
        <uni-icons class="read-num-icon" color="#999999" type="compose" />
        <text>{{ data.updateTime }}</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  props: {
    data: {
      type: Object,
      required: true
    }
  },
  data: () => ({})
};
</script>

<style lang="scss" scoped>
.search-result-item-box {
  margin-bottom: $uni-spacing-col-big;
  // 标题
  .item-title {
    font-size: $uni-font-size-base;
    font-weight: bold;
    color: $uni-text-color-title;
  }

  .item-desc {
    margin-top: $uni-spacing-col-base;
    font-size: $uni-font-size-base;
    color: $uni-color-subtitle;
  }

  // 底部作者 + 阅读量
  .item-desc-bottom {
    margin-top: $uni-spacing-col-base;
    display: flex;
    color: $uni-text-color-grey;
    font-size: $uni-font-size-sm;
    .item-author {
      margin-right: $uni-spacing-row-lg;
    }
    .item-read-num {
      .read-num-icon {
        color: $uni-text-color-grey;
        margin-right: $uni-spacing-row-sm;
      }
    }
  }
}
</style>

search-result-item-theme-2

<template>
  <view class="search-result-item-box">
    <!-- 标题 -->
    <rich-text :nodes="data.title" class="item-title line-clamp-2"></rich-text>
    <view class="item-info-box">
      <view class="item-desc-box">
        <rich-text :nodes="data.description" class="item-desc line-clamp-2"></rich-text>
        <view class="item-desc-bottom">
          <view class="item-author">{{ data.nickname }}</view>
          <view class="item-read-num">
            <uni-icons class="read-num-icon" color="#999999" type="compose" />
            <text>{{ data.updateTime }}</text>
          </view>
        </view>
      </view>
      <image class="item-img" :src="data.pic_list[0]" />
    </view>
  </view>
</template>

<script>
export default {
  props: {
    data: {
      type: Object,
      required: true
    }
  },
  data: () => ({})
};
</script>

<style lang="scss" scoped>
.search-result-item-box {
  margin-bottom: $uni-spacing-col-big;
  // 标题
  .item-title {
    font-size: $uni-font-size-base;
    font-weight: bold;
    color: $uni-text-color-title;
  }

  .item-info-box {
    display: flex;
    margin-top: $uni-spacing-col-base;
    .item-desc-box {
      width: 65%;
      font-size: $uni-font-size-base;
      color: $uni-color-subtitle;
      // 底部作者 + 阅读量
      .item-desc-bottom {
        margin-top: $uni-spacing-col-base;
        display: flex;
        color: $uni-text-color-grey;
        font-size: $uni-font-size-sm;
        .item-author {
          margin-right: $uni-spacing-row-lg;
        }
        .item-read-num {
          .read-num-icon {
            color: $uni-text-color-grey;
            margin-right: $uni-spacing-row-sm;
          }
        }
      }
    }

    .item-img {
      width: 33%;
      height: 70px;
    }
  }
}
</style>

search-result-item-theme-3

<template>
  <view class="search-result-item-box">
    <!-- 标题 -->
    <rich-text :nodes="data.title" class="item-title line-clamp-2"></rich-text>
    <!-- 内容区 - 样式 3 -->
    <view class="item-info-img-box">
      <image v-for="item in data.pic_list" :key="item" class="item-img" :src="item" />
    </view>
    <!-- 底部 -->
    <view class="item-desc-bottom">
      <view class="item-author">{{ data.nickname }}</view>
      <view class="item-read-num">
        <uni-icons class="read-num-icon" color="#999999" type="compose" />
        <text>{{ data.updateTime }}</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  props: {
    data: {
      type: Object,
      required: true
    }
  },
  data: () => ({}),
  created() {
    if (this.data.pic_list.length > 3) {
      this.data.pic_list = this.data.pic_list.splice(0, 2);
    }
  }
};
</script>

<style lang="scss" scoped>
.search-result-item-box {
  margin-bottom: $uni-spacing-col-big;
  // 标题
  .item-title {
    font-size: $uni-font-size-base;
    font-weight: bold;
    color: $uni-text-color-title;
  }

  .item-info-img-box {
    display: flex;
    // 图片
    .item-img {
      width: 33%;
      height: 70px;
      box-sizing: border-box;
    }
    .item-img:nth-child(1n + 1) {
      margin-right: $uni-spacing-row-sm;
    }
  }

  // 底部作者 + 阅读量
  .item-desc-bottom {
    margin-top: $uni-spacing-col-base;
    display: flex;
    color: $uni-text-color-grey;
    font-size: $uni-font-size-sm;
    .item-author {
      margin-right: $uni-spacing-row-lg;
    }
    .item-read-num {
      .read-num-icon {
        color: $uni-text-color-grey;
        margin-right: $uni-spacing-row-sm;
      }
    }
  }
}
</style>

uni.scss

$uni-spacing-col-big: 24px;

search-result-list

<template>
  <view class="search-result-list-container">
    <!-- 循环渲染列表数据 -->
    <block v-for="(item, index) in resultList" :key="index">
      <view class="search-result-item-box">
        <!-- 内容区 - 样式 1 -->
        <search-result-item-theme-1
          v-if="!item.pic_list || item.pic_list.length === 0"
          :data="item"
        />

        <!-- 内容区 - 样式 2 -->
        <search-result-item-theme-2 v-else-if="item.pic_list.length === 1" :data="item" />

        <!-- 内容区 - 样式 3 -->
        <search-result-item-theme-3 v-else :data="item" />

        <!-- / -->
      </view>
    </block>
  </view>
</template>

<style lang="scss" scoped>
.search-result-list-container {
  padding: $uni-spacing-col-lg $uni-spacing-row-lg;
  .search-result-item-box {
    margin-bottom: $uni-spacing-col-big;
  }
}
</style>

7-10:搜索结果 - 处理相对时间

相对时间可以借助 dayjs 进行处理。想要在 uniapp 中使用 dayjs ,那么需要导入 npm 工具(以下内指令需要在项目路径的终端下进行):

  1. 需要在电脑中安装 node 环境,node 安装地址,推荐下载 LTS 版本
    请添加图片描述

  2. 通过 npm init -y 初始化 npm 包管理工具
    请添加图片描述

  3. 通过 npm install dayjs@1.10.4 --save-dev 安装,安装完成,项目中会多出 node_modules 文件夹
    请添加图片描述

  4. 打开 filters/index.js,添加如下代码:

    // 1. 导入dayjs
    import dayjs from 'dayjs';
    // 2. dayjs 默认语言是英文,我们这里配置为中文
    import 'dayjs/locale/zh-cn';
    // 3. 引入 relativeTime
    import rTime from 'dayjs/plugin/relativeTime';
    
    // 4. 加载中文语言包
    dayjs.locale('zh-cn');
    // 5. 加载相对时间插件
    dayjs.extend(rTime);
    
    /**
     * 6. 定义过滤器,通过 dayjs().to(dayjs(val)) 方法把【当前时间】处理为【相对时间】
     */
    export function relativeTime(val) {
      return dayjs().to(dayjs(val));
    }
    
  5. 分别在 search-result-item-theme-1search-result-item-theme-2search-result-item-theme-3 中使用过滤器 <text>{{ data.updateTime | relativeTime }}</text>

7-11:搜索结果 - 高亮搜索结果关键字

title 字段下,搜索关键字都通过 em 标签进行了标记,所以所谓的 搜索关键字高亮 就是 em 标签添加高亮样式

search-result-list

// 更改返回数据样式(行内样式)
      res.list.forEach((item) => {
        item.title = item.title.replace(/<em>/g, "<em style='color:#f94d2a; margin:0 2px'>");
        item.description = item.description.replace(
          /<em>/g,
          "<em style='color:#f94d2a; margin:0 2px'>"
        );
      });

7-12:搜索结果 - 介绍并使用 mescroll-uni 组件

mescroll-uni 是一个专门为 uni app 准备的 pullToRefresh 组件。可以帮助 uniapp 实现 下拉刷新、上拉加载的功能

  1. 在 插件市场 点击 使用 HBuilderX 导入插件

  2. 导入成功,uni-modules 文件夹下会多出 mescroll-ui 文件夹
    请添加图片描述

  3. mescroll-uni 的实现借助了 微信小程序 中的 onPullDownRefresh 和 onReachBottom 这两个 页面 事件。因为 微信小程序组件中没有这两个事件,所以 mescroll-uni页面组件 中的使用方式 不同

  4. 我们现在先来看 mescroll-uni组件 中的使用

  5. mescroll-uni组件 中使用分为两大块:

    1. 组件

      <template>
        <view class="search-result-list-container">
          <!-- 1. 通过 mescroll-body 包裹列表,指定 ref 为 mescrollRef ,监听@init、@down、@up 事件 -->
          <mescroll-body ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback">
            <!-- 循环渲染列表数据 -->
            <block v-for="(item, index) in resultList" :key="index">
      		...
            </block>
          </mescroll-body>
        </view>
      </template>
      
      <script>
      // 2. 导入对应的 mixins
      import MescrollMixin from '@/uni_modules/mescroll-uni/components/mescroll-uni/mescroll-mixins.js';
      export default {
        // 3. 注册 mixins
        mixins: [MescrollMixin],
        methods: {
          // 4. 实现三个回调方法
          /**
           * 首次加载
           */
          mescrollInit() {
            console.log('mescrollInit');
          },
          /**
           * 下拉刷新的回调
           */
          downCallback() {
            console.log('downCallback');
          },
          /**
           * 上拉加载的回调
           */
          upCallback() {
            console.log('upCallback');
          },
        }
      };
      </script>
      
      
    2. 组件所在的页面

      <template>
        <view class="search-blog-container">
          <!-- 搜索结果 -->
          <view class="search-result-box" v-else>
            <!-- 1. 给mescroll-body的组件添加: ref="mescrollItem" (固定的,不可改,与mescroll-comp.js对应)-->
            <search-result-list ref="mescrollItem" :queryStr="searchVal" />
          </view>
        </view>
      </template>
      
      <script>
      // 2. 引入mescroll-comp.js
      import MescrollCompMixin from '@/uni_modules/mescroll-uni/components/mescroll-uni/mixins/mescroll-comp.js';
          
      export default {
        // 3. 注册 mixins
        mixins: [MescrollCompMixin],
      };
      </script>
      
      

7-13:搜索结果 - 实现下拉刷新上拉加载功能

<script>
export default {
    ...
  data() {
    return {
      // 数据源
      resultList: [],
      // 页数
      page: 1,
      // 实例
      mescroll: null,
      // 处理渲染,会回调 down和up 方法,为了避免该问题,定义 init 变量,表示当前是否为首次请求
      isInit: true
    };
  },
  // created() {
  //   不需要主动调用
  //  this.loadSearchResult();
  // },
  // 页面渲染完成之后回调,想要获取组件实例,需要在该回调中进行
  mounted() {
    // 通过 $refs 获取组件实例
    this.mescroll = this.$refs.mescrollRef.mescroll;
  },
  methods: {
    // 4. 实现三个回调方法
    /**
     * List 组件的首次加载
     */
    async mescrollInit() {
      await this.loadSearchResult();
      this.isInit = false;
      // 结束 上拉加载 && 下拉刷新 动画
      this.mescroll.endSuccess();
    },
    /**
     * 下拉刷新的回调,永远都是从第一页开始加载的
     */
    async downCallback() {
      if (this.isInit) return;
      this.page = 1;
      await this.loadSearchResult();
      // 结束 上拉加载 && 下拉刷新
      this.mescroll.endSuccess();
    },
    /**
     * 上拉加载的回调,page开始递增
     */
    async upCallback() {
      if (this.isInit) return;
      this.page += 1;
      await this.loadSearchResult();
      // 结束 上拉加载 && 下拉刷新
      this.mescroll.endSuccess();
    },
    /**
     * 获取搜索数据
     */
    async loadSearchResult() {
      ...
      // 使用下拉刷新上拉加载的赋值策略
      // this.resultList = res.list;
      // 判断是否为第一页数据
      if (this.page === 1) {
        this.resultList = res.list;
      } else {
        this.resultList = [...this.resultList, ...res.list];
      }
    }
  }
};
</script>

7-14:搜索结果 - 处理空数据场景

创建空数据组件empty-data

<template>
  <view class="empty-data-cocntainer">
    <image src="/static/images/empty-data.png" class="img" />
    <view class="txt">暂无数据</view>
  </view>
</template>

<script>
export default {
  name: 'empty-data',
  data() {
    return {};
  }
};
</script>

<style lang="scss" scoped>
.empty-data-cocntainer {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 35%;
  .img {
    width: 64px;
    height: 64px;
  }
  .txt {
    margin-top: $uni-spacing-col-sm;
    color: $uni-text-color-grey;
    font-size: $uni-font-size-base;
  }
}
</style>

在无数据时,进行渲染

search-result-list

<template>
  <view class="search-result-list-container">
    <empty-data v-if="isEmpty"></empty-data>

    <mescroll-body
      v-else
    ></mescroll-body>
  </view>
</template>

<script>
export default {
  ...
  methods: {
...
    /**
     * 获取搜索数据
     */
    async loadSearchResult() {
      ...
      // this.resultList = [];
      // 无数据,显示空数据组件
      if (this.resultList.length === 0) {
        this.isEmpty = true;
      }
    }
  }
};
</script>

7-15:文章搜索 - 细节修复

  1. 搜索历史数据量限制 store/modules/search.js
    /**
     * 添加数据
     */
    addSearchData(state, val) {
      if (!val) return;
      const index = state.searchData.findIndex((item) => item === val);
      if (index !== -1) {
        state.searchData.splice(index, 1);
      }
      // 判断是否超过了最大缓存数量,如果超过了,那么就把最后面的数据进行删除
      if (state.searchData.length > HISTORY_MAX) {
        state.searchData.splice(HISTORY_MAX - 1, state.searchData.length - HISTORY_MAX - 1);
      }

      state.searchData.unshift(val);
      // 调用 saveToStorage
      this.commit('search/saveToStorage');
    },

7-16:总结

在本章节中我们接触到了一个全新的知识点 **全局状态管理工具 vuex **, 我们利用它解决了 数据与组件深度耦合 的问题。

在正式的项目中(无论是 uniapp 项目还是 前端项目) ,**全局状态管理工具 ** 都是非常重要的一个内容。

在处理完了 “历史遗留” 问题之后,我们解决了文章搜索的最后一个模块 搜索结果,我们利用 三个组件分别应对了三种不同的渲染场景,并且通过 行内样式 的形式解决了 关键字高亮的问题。

其中为了处理 pullToRefresh 的场景,我们还接触到了一个新的库 mescroll-uni,利用它很好的解决了 下拉刷新,上拉加载的问题

那么在下一章节,我们则会进入到 文章详情的功能开发。等待我们的又将会是什么样的难题呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/420297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Flutter进阶】聊一聊组件中的生命周期、状态管理及局部重绘

前言 说到生命周期&#xff0c;熟悉Android开发的小伙伴一定第一时间会想到Activity的生命周期&#xff0c;由于在Flutter中一切都是组件&#xff0c;所以组件的生命周期其实是类似的。 在这个过程中组件的状态——State就非常重要&#xff0c;它记录这整个组件内可变部分的状…

【SSM整合】1—Spring和Mybatis整合

⭐⭐⭐⭐⭐⭐ Github主页&#x1f449;https://github.com/A-BigTree 笔记链接&#x1f449;https://github.com/A-BigTree/Code_Learning ⭐⭐⭐⭐⭐⭐ Spring专栏&#x1f449;https://blog.csdn.net/weixin_53580595/category_12279588.html SpringMVC专栏&#x1f449;htt…

linux安装kafka

目录 目录 一.安装包准备&#xff1a; 二.解压安装&#xff1a; 先将该安装包放入到/opt/install目录&#xff1a; 解压该文件到soft目录中&#xff1a; 改名&#xff0c;方便后续使用&#xff1a; 三修改其中配置和配置环境变量&#xff1a; 3.1 修改/opt/soft/kafka2…

camunda工作流引擎开发架构

Camunda的开发架构可以分为前端开发架构和后端开发架构。 前端开发架构&#xff1a; Camunda前端使用Angular框架进行开发&#xff0c;主要包括以下组件&#xff1a; 1、Cockpit&#xff1a;流程监控和管理界面。 2、Tasklist&#xff1a;任务管理和审批界面。 3、Admin&…

答题积分小程序云开发实战-开篇:项目介绍以及效果图

答题积分小程序云开发实战 开篇:项目介绍以及效果图 前言 我也看过不少的册子或者文章,大部分都很优秀,但也有的就长篇累牍,从时代背景讲起,复述各种基本概念、底层原理......嗯,看似很高级~ 但我阅读的时候,给我的感觉是,把你绕晕、把你劝退的感觉,相信大家都有同感,…

C++输入输出、缺省参数、函数重载、引用【C++初阶】

目录 一、C输入&输出 二、缺省参数 1、概念 2、分类 &#xff08;1&#xff09;全缺省 &#xff08;2&#xff09;半缺省 三、函数重载 1、概念 2、原理------名字修饰 一、C输入&输出 在C语言中&#xff0c;我们常用printf和scanf这两个函数进行输入输出。 …

产品-Axure9(英文版),.rp文件与.rplb文件的转换与区分

文章目录1、区分2、相互转换2.1 rp转为rplb2.1 rplb转为rp1、区分 rp文件是文档文件&#xff0c;可以理解为作品文件&#xff0c;自己的工作输出就是rp文件&#xff0c;图标如下。 rplb文件是库文件&#xff0c;是在制作文件过程中一个快捷库&#xff0c;图标如下 在点击绿色…

GitHub 上诞生了一个可视化低代码神器

作为开发者&#xff0c;你是否早已厌倦了日复一日的“增删改查”&#xff0c;每天都在重复造轮子&#xff0c;今天给大家推荐一款开源、靠谱、实用的低代码开发平台 -- ILLA Builder。 产品介绍 ILLA Builder 是 ILLA 的核心产品&#xff0c;是一款开源的低代码开发工具。通过…

ROS话题通信自定义+发布订阅代码--03

话题通信自定义msg 在 ROS 通信协议中&#xff0c;数据载体是一个较为重要组成部分&#xff0c;ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是&#xff0c;这些数据一般只包含一个 data 字段&#xff0c;结构的单一意味…

C++实现JPEG格式图片解析(附代码)

在网上看了好多解析JPEG图片的文章&#xff0c;多多少少都有问题&#xff0c;下面是我参考过的文章链接&#xff1a; 首先&#xff0c;解析的步骤1.读取文件的信息2.Huffman编码解码3.直流交流编码解析然而&#xff0c;读取多少个88矩阵才能解析出一个MCU呢&#xff1f;4.反量化…

8年测试老鸟总结,接口自动化测试测试用例编写(全覆盖场景)

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 自动化测试&#xf…

15-721 Chapter 6 索引

最先是解释了一个古老的&#xff0c;现在没什么人用数据结构----T-tree&#xff0c;因为现代的cpu到cache和到memory差异巨大&#xff0c;同时memory的容量也变大了。 T-tree 两个key标志着范围&#xff0c;决定到哪里找key&#xff0c;然后存的都是指针&#xff0c;指向pare…

CANopen | 对象字典OD 05 - 创建对象字典变量,映射到RPDO

文章目录一、前言二、实验目的三、对象字典OD四、通过RPDO修改变量rx_Value4.1、NMT指令让CANopen从站进入操作状态4.2、RPDO修改变量rx_Value一、前言 该章节的源代码地址&#xff1a;github 以上摘自《CANopen_easy_begin》的第7章。 二、实验目的 CANopen从站有一个变量…

【博学谷学习记录】超强总结,用心分享 | 架构师 MySql扩容学习总结

文章目录1. 停机方案2.停写方案3.日志方案4.双写方案&#xff08;中小型数据&#xff09;5.平滑2N方案&#xff08;大数据量&#xff09;1. 停机方案 发布公告 为了进行数据的重新拆分&#xff0c;在停止服务之前&#xff0c;我们需要提前通知用户&#xff0c;比如&#xff1a…

网络io与select,poll,epoll

一个形象的类比 水龙头等水 水龙头就是内核进程 等水复制到内核区 学生就是进行io的进程或线程 阻塞io 学生在那里 等水来 非阻塞io 学生看数据没准备好,先回寝室,一会儿再过来检查下,看水准备好没 多路复用io 阿姨帮忙看着水龙头,等来水的时候通知学生 前面三个都是同步…

HQChart实战教程60-如何定制十字光标输出内容

HQChart实战教程60-如何定制十字光标输出内容 十字光标效果图步骤:1. 注册事件2. 外部格式化输出内容Y 轴输出说明X轴输出说明HQChart插件源码地址完整的demo源码十字光标 当鼠标或手势在K线上移动的时候, 会出现一个十字线,已经X轴和Y轴对应数值的输出。X轴输出日期+时间 …

2.1.1网络io与io多路复用select/poll/epoll

关于网络io&#xff0c;我们可以通过一个服务端-客户端的示例来了解&#xff1a; 这是一段TCP服务端的代码&#xff1a; #include <stdio.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include &l…

Android系统启动流程--zygote进程的启动流程

在上一篇init进程启动流程中已经提到&#xff0c;在init中会解析一个init.rc文件&#xff0c;解析后会执行其中的命令来启动zygote进程、serviceManager进程等&#xff0c;下面我们来看一下&#xff1a; //文件路径&#xff1a;system/core/init/init.cppstatic void LoadBoot…

电子商务转化率对你来说有多重要?

有许多电子商务企业遇到了瓶颈期&#xff0c;低转化率并不总是表明您的业务出了大问题&#xff0c;但它们确实表明您可以做得更多&#xff0c;赚得更多。在文中&#xff0c;我们将讨论电子商务转化率对你的重要性&#xff0c;以及提高电子商务转化率的最佳久经考验的方法。 一、…

如何选择IT培训机构?

作为学习IT技术的一种方式、平台&#xff0c;培训班存在已久。而作为国内培训机构的老大哥&#xff0c;北大青鸟于1999年成立&#xff0c;是IT职业教育的开创者&#xff0c;专注于软件、网络、营销等各个IT技术领域&#xff0c;为IT行业输送了奖金百万技术人才。24年以来&#…