分类
XMLHttpRequest

XMLHttpRequest

获取类型及如何获取

responseType值xhr.response数据类型获取方式
“”(默认)String字符串responseText
“text”String字符串responseText
“document”Document对象responseXML
“json”javascript对象response
“blob”Blob对象response
“arrayBuffer”ArrayBuffer对象response

如何追踪请求当前状态

xhr.onreadystatechange = function () {
    switch(xhr.readyState){
      case 1://OPENED
        //do something
            break;
      case 2://HEADERS_RECEIVED
        //do something
        break;
      case 3://LOADING
        //do something
        break;
      case 4://DONE
        //do something
        break;
    }
状态描述
0UNSENT(初始状态,未打开)此时xhr对象被成功构造open()方法还未被调用
1OPENED (已打开,未发送)open()方法已被成功调用,send()方法还未被调用。。注意:只有xhr处于OPENED状态,才能调用xhr.setRequestHeader()和xhr.send(),否则会报错
2HEADERS_RECEIVED (已获取响应头)send()方法已经被调用, 响应头和响应状态已经返回
3LOADING (正在下载响应体)响应体(response entity body)正在下载中,此状态下通过xhr.response可能已经有了响应数据
4DONE (整个数据传输过程结束)整个数据传输过程结束,不管本次请求是成功还是失败

如何设置请求的超时时间

xhr.timeout=0 单位毫秒,默认0为不超时

请求开始

xhr.send()的时候

请求结束

xhr.loadend触发的时候

注意点

  • send之后还可以设置timeout
  • xhr为一个sync同步请求是,xhr.timeout必须设置为0。

同步请求

尽量不使用,可能会导致页面一直阻塞,xhr.readyState由2变成3时,并不会触发onreadystatechange事件,xhr.upload.onnprogress和xhr.onprogress事件也不会触发。

open(method, url [, async = true [, username = null [, password = null]]])

解释
methodGET/POST/HEADER不区分大小写
url可以是相对地址,也可以是绝对地址如http://zhuishao.net/example.php
async默认为true即为异步请求,async=false是同步请求

同步请求限制

  • xhr.timeout必须为0
  • xhr.withCredentials必须为false
  • xhr.responseType必须为””

获取上传下载进度

  • 上传事件触发的是xhr.upload对象的onprogress事件
  • 下载触发的是xhr对象的onprogress事件
xhr.onprogress = updateProgress;
xhr.upload.onprogress = updateProgress;
function updateProgress(event) {
    if (event.lengthComputable) {
      var completedPercent = event.loaded / event.total;
    }
 }

可以发送什么数据

xhr.send(data);

  • ArrayBuffer
  • Blob
  • Document
  • DOMString
  • FormData
  • null

get方法一般不传参,传了也会被置为null

data值content-type的默认值
Document同时也是HTML Documenttext/html;charset=UTF-8
Docuemnt不是HTMLDocumentapplication/xml;charset=UTF-8
DOMStringtext/plain;charset=UTF-8
FormDatamultipart/form-data; boundary=[xxx]
其他类型不会设置默认值

xhr.withCredentials与CORS

浏览器跨域发送请求不能发送任何认证信息(cookies和HTTP authentication schemes),除非xhr.withCredentials为true,默认值是false。

浏览器端withCredentials服务器端
Access-Control-Allow-Origin
服务器端
Access-Control-Allow-Credentials
false(默认)随意随意
true请求页面域名,不能是*true

事件及触发条件

事件触发条件
onreadystatechange每当xhr.readyState改变时触发;但xhr.readyState由非0值变为0时不触发。
onloadstart调用xhr.send()方法后立即触发,若xhr.send()未被调用则不会触发此事件。
onprogressxhr.upload.onprogress在上传阶段(即xhr.send()之后,xhr.readystate=2之前)触发,每50ms触发一次;xhr.onprogress在下载阶段(即xhr.readystate=3时)触发,每50ms触发一次。
onload当请求成功完成时触发,此时xhr.readystate=4
onloadend当请求结束(包括请求成功和请求失败)时触发
onabort当调用xhr.abort()后触发
ontimeoutxhr.timeout不等于0,由请求开始即onloadstart开始算起,当到达xhr.timeout所设置时间请求还未结束即onloadend,则触发此事件。
onerror在请求过程中,若发生Network error则会触发此事件(若发生Network error时,上传还没有结束,则会先触发xhr.upload.onerror,再触发xhr.onerror;若发生Network error时,上传已经结束,则只会触发xhr.onerror)。注意,只有发生了网络层级别的异常才会触发此事件,对于应用层级别的异常,如响应返回的xhr.statusCode是4xx时,并不属于Network error,所以不会触发onerror事件,而是会触发onload事件。

事件触发顺序

正常触发流

  1. xhr.onreadystatechange(之后每次变化都会触发一次)
  2. xhr.onloadstart
  3. xhr.upload.onloadstart
  4. xhr.upload.onprogress
  5. xhr.upload.onloadend
  6. xhr.onprogress
  7. xhr.onload
  8. xhr.onloadend

发生abort/timeout/error异常的处理

  1. 一旦发生abort,timeout,error异常,先立即终止当前请求
  2. readystate置为4触发xhr.onreadystatechange
  3. 若上传没有结束依次触发xhr.upload.onprogress,xhr.upload.[onabort|ontimeout|onerror],xhr.upload.onloadend
  4. 触发xhr.onporgress事件
  5. 触发xhr.[onabort|ontimeout|onerror]事件
  6. 触发xhr.onloadend事件

成功回调

更倾向xhr.onload

xhr.onload = function () {
    //如果请求成功
    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
      //do successCallback
    }
  }

分类
webpack

webpack配置学习

resolve

resolve.extensions

extensions:[‘.js’,’.vue’,’.json’],

我们在路由中引入组件时可以这样import Hello from ‘@components/Hello’;

resolve.alias

alias:{
    'element-ui': path.resolve(__dirname, '../')
}

resolve.modules

modules: [‘node_modules’]在引入的时候就不用

import ‘node_modules/util’,可以用

import ‘util’;

环境变量

静态环境变量的创建

在.env文件中

VUE_APP_API_BASE_URL=/api

动态环境变量的创建

在vue.config.js

const moment = require('moment');
process.env.VUE_APP_BUILD_TIME = moment().format('YYYY-M-D HH:mm:ss');

环境变量的使用

在html代码中

<span style="display: none;">ver:<%= VUE_APP_VERSION %>;build-time:<%= VUE_APP_BUILD_TIME%>;build-user:<%=VUE_APP_BUILD_USER%></span>

在vue代码中

data() {
    return {
      // 公钥
      publicKey: process.env.VUE_APP_PUBLIC_KEY,

    };
  },

在js中

const baseURL = process.env.VUE_APP_API_BASE_URL;
分类
babel

babel插件作用

babel本身不具有任何转化功能,他把转化的功能都分解到一个个plugin里面。因此当我们不配置任何插件是,经过babel的代码和输入是相同的。

babel-plugin-syntax-trailing-function-commas

定义方法时,foo(param1,param2,)最后一个参数增加逗号会使babel报错,该语法插件能让babel不报错。

babel-plugin-transform-es2015-arrow-functions

转义箭头函数

babel-plugin-transform-runtime

代码从转化前定义转化函数改成引用转化函数,减少重复定义,需要以babel-runtime为依赖(dependence)

分类
解决方案

国际化解决方案(vue)

目录

  1. 国际化自定义配置
  2. 项目中使用需要注意的地方
  3. 国际化过程中节约时间的小插件
  4. 组件库的国际化

国际化配置

i18n下载

npm i vue-i18n

引入

main.js文件配置

// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
// 国际化
import i18n from './i18n';
// 启动程序
import { created } from './core/bootstrap';

new Vue({
  router,
  store,
  created,
  i18n,
  render: h => h(App),
}).$mount('#app');

i18n目录情况

./src/i18n/
├── index.js
└── lang
    ├── README.md
    ├── en-GB                                # 英式英文 文件夹
    │   ├── business
    │   ├── common
    │   │   └── index.js                   # 通用信息
    │   └── pages                           # 项目页面标签
    │       ├── create.js                   # 新建流程页面
    │       ├── login.js                    # 登录页面
    │       ├── todo.js                     # 待办流程页面
    │       └── view.js                     # 我的申请页面
    ├── en-US                                # 美式英文 文件夹
    │   ├── business
    │   ├── common
    │   │   └── index.js
    │   └── pages
    │       ├── create.js
    │       ├── login.js
    │       ├── todo.js
    │       └── view.js
    └── zh-CN                               # 简体中文 文件夹
        ├── business
        ├── common
        │   └── index.js
        └── pages
            ├── create.js
            ├── login.js
            ├── todo.js
            └── view.js

i18n文件夹下的index.js文件配置国际化详细信息

import Vue from 'vue';
import VueI18n from 'vue-i18n';

import zhCN from './lang/zh-CN';
import enUS from './lang/en-US';
import enGB from './lang/en-GB';

Vue.use(VueI18n);

const messages = {
  'zh-CN': zhCN,
  'en-US': enUS,
  'en-GB': enGB,
};
export const defaultLang = 'zh-CN';
const i18n = new VueI18n({
  // 当前语言,从缓存中读取
  locale: defaultLang,
  // 回退语言-当找不到时,使用回退语言
  // fallbackLocale: defaultLang,
  messages,
});

export default i18n;

function setI18nLanguage(lang) {
  i18n.locale = lang;
  document.querySelector('html').setAttribute('lang', lang.toLowerCase());
  return lang;
}

export function loadLanguageAsync(lang = defaultLang) {
  return new Promise((resolve) => {
    // 缓存语言设置
    if (i18n.locale !== lang) {
      return resolve(setI18nLanguage(lang));
    }
    return resolve(lang);
  });
}

i18n->lang->en-GB->index.js

import login from './login';
import create from './create';
import personCenter from './personCenter';

export default {
  login,
  create,
  personCenter,
};

i18n->lang->en-GB->login.js

export default {
  title: 'welcome login BPM system',
  bt: 'Login',
  placeholder: {
    username: 'please input user name',
    password: 'please input password',
  },
  message: {
    username: 'please input user name',
    password: 'please input password',
  },
  error: 'login fail',
};

流程图-执行流程

qi ye wei xin jie tu 16056018244734 - 国际化解决方案(vue)

启动事件(core => bootstrap.js)

import store from '@/store/';
import auth from '@/utils/auth';
// 应用程序事件处理
export function created() {
  // 从本地存储中恢复语言设置
  store.dispatch('system/app/setLang', auth.getLocal());
}

获取本地语言函数(utils=>auth.js)

export default {
  /**
   * 获取当前语言
   */
  getLocal() {
    return localStorage.local || 'zh-CN';
  },
  /**
   * 设置语言
   */
  setLocal(local) {
    if (localStorage.local !== local) {
      localStorage.local = local;
    }
  },
};

执行国际化函数(store=>system=>app.js)

import {
  // i18n
  APP_LANGUAGE,
} from '@/store/mutation-types';
import auth from '@/utils/auth';
import { loadLanguageAsync } from '@/i18n';
const app = {
  namespaced: true,
  state: {
    // 切换语言
    lang: 'zh-CN',
  },
  mutations: {
    /**
     * 设置语言
     * @param {*} state
     * @param {*} lang
     */
    [APP_LANGUAGE](state, lang) {
      state.lang = lang;
      auth.setLocal(lang);
    },
  },
  actions: {
    setLang({ commit }, lang) {
      return new Promise((resolve, reject) => {
        commit(APP_LANGUAGE, lang);
        loadLanguageAsync(lang).then(() => {
          resolve();
        }).catch((e) => {
          reject(e);
        });
      });
    },
  },
};

export default app;

语言配置名称(store=>mutation-types.js)

export const APP_LANGUAGE = 'app_language';

在项目中进行语言切换

this.$store.dispatch('system/app/setLang', val);

项目中使用需要注意的地方

浏览页标题

某个路由

import loaded from '@/utils/util.import';

export default [
  // 默认页面
  {
    path: '/home',
    name: 'home',
    meta: {
      title: '主页',
      dataKey: 'common.home',// 若使用国际化用这个字段
      tabId: 'home',
      icon: 'home',
      closable: false,
    },
    component: loaded('Home'),
  },
  // 刷新页面 必须保留
  {
    path: '/refresh',
    hidden: true,
    component: loaded('system/function/refresh'),
  },
];

路由拦截函数

import i18n from '@/i18n';
export function routerAfterEachFunc(to) {
  util.title(i18n.t(to.meta.dataKey));
}

一般表格表头-可以放在计算属性中

computed: {
    columns() {
      const columns = [
        {
          title: this.$t('pages.create.columns.sn'),
          align: 'center',
          customRender: (text, record, index) => (this.pageNum - 1) * this.pageSize + index + 1,
        },
        {
          title: this.$t('pages.create.columns.subject'),
          dataIndex: 'subject',
        },
        {
          title: this.$t('pages.create.columns.name'),
          scopedSlots: { customRender: 'lineEllipsis' },
          dataIndex: 'processNameCn',
        },
        {
          title: this.$t('pages.create.columns.createTime'),
          dataIndex: 'createTime',
        },
        {
          title: this.$t('pages.create.columns.action'),
          key: 'action',
          width: 100,
          scopedSlots: { customRender: 'action' },
        },
      ];
      return columns;
    },
  },

2121-04-09更新

columns还是可以放在data中,但是计算属性进行变更

langColumns() {
      this.columns.forEach((item) => {
        item.title = this.$t(`columns.${item.dataIndex}`);
      });
      return this.columns;
    },

动态列表格表头-使用slot

data() {
    return {
        columns: [{
            slots: { title: 'noTitle' },
            dataIndex: 'no',
        }]
    }
}
<a-table :columns="columns">
    <span slot="noTitle">{{$t('columns.sn')}}</span>
</a-table>

国际化过程中节约时间的小插件

流程如下

vscode搜索中文:(.[\u4E00-\u9FA5]+)|([\u4E00-\u9FA5]+.)

qi ye wei xin jie tu a206e6d2cc1342e0b98fc6891bef73bf 569x1024 - 国际化解决方案(vue)

从vscode中找出所有含有汉字的代码->用小插件找到去重的汉字列表给专业人员翻译->开发人员自顾自的编写i18n文件->专业人员翻译好了,给出一个汉字英文对照表->使用小插件把中文i18n文件和汉字英文对照表放上去生成英文i18n文件->复制粘贴完成

guo ji hua xiao cha jian 1 1 - 国际化解决方案(vue)
guo ji hua xiao cha jian 2 - 国际化解决方案(vue)

小插件地址

https://github.com/zhuishao/hundredstest/blob/master/140.html

组件库的国际化

需求:没用国际化的项目默认显示中文,有国际的的项目使用该组件可配置vue-i18n国际化。

  1. 将https://github.com/zhuishao/ec-zs-components下的src目录复制到组件库中
  2. 在有语言的组件中加入mixins: [Locale], import Locale from ‘ec-zs-components/src/mixins/locale’,通过t(xx.xx.xx)使用
  3. ec-zs-components的引用其实是加了别名alias在vue.config.js中加入
 configureWebpack: {
        resolve: {
            alias: {
                'ec-zs-components': path.resolve(__dirname, './'),
            },
            extensions: ['.js', '.vue', '.json'],
        }
    },
  1. 向外发布的时候需要将语言包暴露出去以供使用在package的script中加入构建命令
"scripts": {
    "lib": "vue-cli-service build --target lib --name ec-zs-components --dest lib --entry packages/index.js && babel src/locale/lang --out-dir lib"
  },

在命令行执行babel命令需要加入babel-cli依赖

使用这个组件库并与i18n产生链接

在main.js国际化配置的基础上引入插件

import i18n from './i18n';
import EcZsComponent from 'ec-zs-components';
import 'ec-zs-components/lib/ec-zs-components.css';
Vue.use(EcZsComponent, {
  i18n: (key, value) => i18n.t(key, value)
});
分类
CSS

一种新动画 我称之为组装。

这种动效也是我灵光一闪想出来的,当然需要一些基础的沉淀。

动画效果

assemble - 一种新动画 我称之为组装。

技术储备

这里需要两种技术沉淀,第一种是transition-timing-function中的steps,它可以使动画像阶梯状呈现,就像一部连续的电影取其中的几帧一样。第二种是-webkit-mask遮罩,在css中若对一个元素使用遮罩,那被遮元素则只能看见一部分内容,这部分就是遮罩图片不透明的部分。如果遮罩图片有半透明的部分,则也会显示出来,只不过显示出来的部分也是半透明的。

思路

首先我们假设有这样一张遮罩图片

0000.20.40.60.811111
00000.20.40.60.81111
000000.20.40.60.8111
里面的数字代表透明度

然后我们把遮罩元素的大小定义为宽400%高100%。

现在我们遮罩的位置在初始位置,所以我们是看不见任何东西的。

那如果我们遮罩的位置向右移动一格呢?我们会发现右上角有一个小方块能显示出来了,按照规律一步步的向右移动,最后该元素就会完全显示!

好了,我们现在要解决一个每次移动都精准的只移动一格的问题。

首先我们要让遮罩移动,则一定会改变-webkit-mask-position,它的作用规则与background-position是一样的。无论背景大小定义的有多大,只要他们的值定义为100%,那就会移动到最右边。然后steps函数就派上了用上,我们设定移动到最右边需要9步,那么每一次截图出来的动画正好是移动一格的位置。

 @keyframes wpSpan{
            0%{
                -webkit-mask-position:0;
            }
            100%{
                -webkit-mask-position:100%;
            }
        }
.x1{
            height:510px;
            background-image: url(./img/border-1.png);
            background-size:cover;
            -webkit-mask-image:url(./svg/8.svg);
            -webkit-mask-size: 400% 100%;
            visibility: hidden;
            -webkit-mask-position:0;
            transition: -webkit-mask-position 1s steps(9,end),visibility 0s 1s;
        }
.x1.active{
            visibility:visible;
            -webkit-mask-position:100%;
            transition: -webkit-mask-position 1s steps(9, end);
        }

图片选择

其实这样看来,最后使这个动画实现的决定性因素其实是图片。我曾经做过这样的探索,如果使用canvas创建一个这样一个图片并转换为base64格式能不能实现这样的效果呢?

结论是实现效果不理想,因为图片太小,在放大后小方格就显示不出来了,如果图片太大,那获取图片的代价有点大,而且有失真的缺点。

所以我选择的是svg图片,如果要制作上述的遮罩,只要设定宽12高3,在非透明的地方放入小方块。

普通的svg图片是不能自适应大小的,所以我们要设置如下

<svg width="12" height="3" viewBox="0 0 12 3"
     preserveAspectRatio="none"
     xmlns="http://www.w3.org/2000/svg">
。。。。。
</svg>

结束语

如果想要源代码和这个svg图片的话,欢迎访问我的github

https://github.com/zhuishao/hundredstest/blob/master/138.html

分类
总结

阶段性回顾(1)

记忆力一直不是我的强项,但有些东西总是值得回忆的。记录下前端生涯走过的路,获得的进步。多年以后再来看看也是一件十分有趣的事。

初战:一个复杂的流程系统

说它是初战是因为这是第一个,我作为唯一的前端负责开发的系统,在此之前也做了一些打杂性的工作,比如说学习了vue,改了改另一个系统的ui,参与了又另一个系统的接口联调及页面开发,也许是前端缺人,也许是同事老板的信任,也许两者都有,这个任务落到了我头上。

这个系统主要考验了我的程序思维,以这个系统中的一个流程来举例,一个流程至少有17个步骤,每个步骤可能由不同的人来操作,并且有并行的操作,即同一个步骤可能有多个人来完成,这样结果也就从单个变成了数组。同时,需要把这17个步骤放在6个页面当中,这就需要判断在这个页面中执行的是第几个步骤。在这样的一个流程中分为两条线路来操作,分别是填写流和审批流。当你处在填写流时,你可以执行的操作是提交,暂存,返回。竟然还有个暂存,这是不是意味着我还得把写的东西存下来,放进草稿。当你处在审批流时,你可以同意,驳回,转办,加签。这个加签也是比较烦的东西,因为加签后流程的步骤没有改变,但是加签的人可以输入意见然后提交,提交后在审批记录中有加签人的意见。而且这是一个长流程,在流程的后期你也得看到前面的流程。

总之,经过这个系统的洗礼,我感觉技术实力有了很大的进步,比如下载文件的操作、echart的操作(后期有报表)、判断超时时间是前端设置的时间超时取消调用还是后端的ngix服务器超时取消返回给前端(这样就能愉快的甩锅了)。再比如如何在页面切换时刷新,如何优化点击同一个流程时会返回已打开的流程而不是再次重新打开。熟悉了下ant-design-vue,和vue,vue-router,store等。

再战:一个订单系统

虽然上一个系统的业务十分之复杂,我也把它做了出来,但是一看单单一个页面的代码量,我的天!竟然有两千多行。显然这是不行的,在这个系统里,我得改正。于是组件化开始了。每个页面大致都有一个高级检索,于是把它做成了组件。在这个组件中添加了键盘快捷搜索,展开收起,页面自适应。把所有的搜索表单表单都放在一个组件内,通过name判断是否应该显示。这样我就能方便调用数据字典接口,为了更方便的调用数据字典,实现了只要在一个数组中加入一个单词,就能自动调用该单词的数据字典。

同时学习了一些组件通信的方法,比如如何暴露组件函数,作用域插槽的运用,子组件使用外部方法后接收结果进行再处理,利用这些知识做了一个动态增减表单项组件,成为了博客的内容。

git上的提升,能比较熟练的运用git操作,从之前的fix,merge提示记录改成符合angular团队规范的提交记录格式,从此每一个项目的gitlab都能成为航海日志,并且减少了分支至2条,一个是master一个是当前版本的分支。

一个比较快速的项目

这是一个知识教训库,在这个项目中的提升主要是弄清了env配置,组件运用更加全面,没有再出现一个页面复制到别的页面的情况。

用时三个星期,也算锻炼了自己快速实现的能力吧。

一个改密码的项目

在上一个项目和这个项目中间学习了一波webpack,所以这个项目的技术含量会有一些提升。首先此项目要在手机端和电脑端显示,通常我们会启动两个项目来分别写手机和电脑端。但是这个项目并不大,我用了一些css技巧识别了手机和pc的区别(没有用宽度识别)。在媒体查询下分别写他们的css,做到了一份代码,多端使用。然后就是国际化,rsa加密,并照着友商的改密码项目手写了一个人机校验插件,并把rsa功能放进该插件里。于是取名叫做rsa-verify,现在能从npm搜到。

优化的部分也从减少包体积入手,比如去掉不必要的包,cdn加速,从首页渲染速度入手的优化有包分割,ant-design-vue的按需引入,ant-design-icon的按需引入。

期间读了本身框架的路由逻辑,因此能比较从容的改路由白名单,防止地址随意跳转,也能配置与后台交互时header的属性。

空闲时学习的知识

正则

在改密码项目中修改密码用到的各种规则都是用正则办到的,正则就像一个好用的工具,就要系统的学习,之后对项目的影响也是潜移默化的。

svg

其中学习了svg如何绘制图形,如何制作smil动画,svg滤镜,其中使用了svg滤镜动画在改密码项目中做了一个噪音滤镜改造过的水波纹点击动效,不过惨遭ui删除555。到底啥时候能有他一展身手的机会啊。。。

canvas

只会照着api用,解决了一个后台传过来白底背景图片放到蓝色基调的页面不好看的问题。解决方案是这样的:白底=>黑底,就是图片数据扣到canvas上,形式是一串长一维数组,规律rgba,改rgb,值=|255-数据|。有了一个黑底的图片。放到页面上。再利用滤色模式screen规律,黑色碰上别的颜色计算出来的值都是那一个别的颜色。于是mix-mode-blend:screen;加上background-color:需要的颜色。其实还可用svg滤镜实现,feColorMatrix滤镜的luminanceToAlpha模式色调翻转,白边黑。feFlood蒙上需要的颜色。feBlend的mode设置为screen将两者的结果结合生成最终的滤镜效果。这种方式不需要改动页面,只需要在外边放个svg滤镜输出id。在该图片的类中加一个filter:url(#id)即可。

学习来源

阿里来的带我的师傅,姓康。也是我真正意义上的技术入门导师,所以我叫他师傅,而不是康师傅。

张鑫旭博客,张鑫旭css世界,张鑫旭选择器世界

mdn,css tricks,html草案,css草案

javascript高级程序设计第三版

下一步的方向

由于在大二起我就看了张鑫旭的博客一路走来,所以css基础应该算是不错的。我会从javascript入手再巩固一遍基础,我买了高性能javascript,和你不知道的javascript上中下这四本书来阅读。

会尝试滤镜的更多可能性。

从项目角度,希望解决service worker离线缓存带来的项目上线后再次上线用户需要清缓存才能看到更新的问题。