浅谈Vue页面级缓存解决方案feb-alive(上)

feb-alive

github地址
体验链接

使用理由

  • 开发者无需因为动态路由或者普通路由的差异而将数据初始化逻辑写在不同的钩子里beforeRouteUpdate或者activated
  • 开发者无需手动缓存页面状态,例如通过localStorage或者sessionStorage缓存当前页面的数据
  • feb-alive会帮你处理路由meta信息的存储与恢复

为什么开发feb-laive?

当我们通过Vue开发项目时候,是否会有以下场景需求?

  • /a跳转到/b
  • 后退到/a时候,希望从缓存中恢复页面
  • 再次跳转到/b时,分两种情况
    • 情况一: 通过链接或者push跳转,则希望重新创建/b页面,而不是从缓存中读取
    • 情况二: 如果点击浏览器自带前进按钮,则还是从缓存中读取页面。

这个场景需求着重强调了缓存,缓存带来的好处是,我上次页面的数据及状态都被保留,无需在从服务器拉取数据,使用户体验大大提高。

尝试用keep-alive实现页面缓存

<keep-alive>
 <router-view></router-view>
</keep-alive>

so easy但是理想很完美,现实很残酷

存在问题

-/a跳到/b,再跳转到/a 的时候,页面中的数据是第一次访问的/a页面,明明是链接跳转,确出现了缓存的效果,而我们期望的是像app一样开启一个新的页面。

  • 同理动态路由跳转/page/1->/page/2因为两个页面引用的是同一个组件,所以跳转时页面就不会有任何改变,因为keep-alive的缓存的key是根据组件来生成的(当然Vue提供了beforeRouteUpdate钩子供我们刷新数据)
  • 总结:keep-alive的缓存是==组件级别==的,而不是==页面级别==的。

举个应用场景

例如浏览文章页面,依次访问3篇文章

  • /artical/1
  • /artical/2
  • /artical/3

当我从/artical/3后退到/artical/2时候,由于组件缓存,此时页面还是文章3的内容,所以必须通过beforeRouteUpdate来重新拉取页面2的数据。(注意此处后退不会触发组件的activated钩子,因为两个路由都渲染同个组件,所以实例会被复用,不会执行reactivateComponent)

如果你想从/artical/3后退到/artical/2时,同时想恢复之前在/artical/2中的一些状态,那么你还需要自己针对/artical/2中的所有状态数据进行存储和恢复。

综上:keep-alive实现的组件级别的缓存和我们想象中的缓存还是有差距的,keep-alive并不能满足我们的需求。

==针对这些问题,所以feb-alive插件诞生了==

由于feb-alive是基于keep-alive实现的,所以我们先简单分析一下keep-alive是如何实现缓存的

export default {
 name: 'keep-alive',
 abstract: true,

 props: {
 include: patternTypes,
 exclude: patternTypes,
 max: [String, Number]
 },

 created () {
 this.cache = Object.create(null)
 this.keys = []
 },

 destroyed () {
 for (const key in this.cache) {
  pruneCacheEntry(this.cache, key, this.keys)
 }
 },

 mounted () {
 this.$watch('include', val => {
  pruneCache(this, name => matches(val, name))
 })
 this.$watch('exclude', val => {
  pruneCache(this, name => !matches(val, name))
 })
 },

 render () {
 // 获取默认插槽
 const slot = this.$slots.default
 // 获取第一个组件,也就和官方说明的一样,keep-alive要求同时只有一个子元素被渲染,如果你在其中有 v-for 则不会工作。
 const vnode: VNode = getFirstComponentChild(slot)
 // 判断是否存在组件选项,也就是说只对组件有效,对于普通的元素则直接返回对应的vnode
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  // 检测include和exclude
  const name: ?string = getComponentName(componentOptions)
  const { include, exclude } = this
  if (
  // not included
  (include && (!name || !matches(include, name))) ||
  // excluded
  (exclude && name && matches(exclude, name))
  ) {
  return vnode
  }

  const { cache, keys } = this
  // 如果指定了子组件的key则使用,否则通过cid+tag生成一个key
  const key: ?string = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
  // 判断是否存在缓存
  if (cache[key]) {
  // 直接复用组件实例,并更新key的位置
  vnode.componentInstance = cache[key].componentInstance
  remove(keys, key)
  keys.push(key)
  } else {
  // 此处存储的vnode还没有实例,在之后的流程中通过在createComponent中会生成实例
  cache[key] = vnode
  keys.push(key)
  // 当缓存数量大于阈值时,删除最早的key
  if (this.max && keys.length > parseInt(this.max)) {
   pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
  }
  // 设置keepAlive属性,createComponent中会判断是否已经生成组件实例,如果是且keepAlive为true则会触发actived钩子。
  vnode.data.keepAlive = true
 }
 return vnode || (slot && slot[0])
 }
}

keep-alive是一个抽象组件,组件实例中维护了一份cache,也就是以下代码部分

created () {
 // 存储组件缓存
 this.cache = Object.create(null)
 this.keys = []
}

由于路由切换并不会销毁keep-alive组件,所以缓存是一直存在的(嵌套路由中,子路由外层的keep-alive情况会不一样,后续会提到)

继续看下keep-alive在缓存的存储和读取的具体实现,先用一个简单的demo来描述keep-alive对于组件的缓存以及恢复缓存的过程

let Foo = {
 template: '<div class="foo">foo component</div>',
 name: 'Foo'
}
let Bar = {
 template: '<div class="bar">bar component</div>',
 name: 'Bar'
}
let gvm = new Vue({
 el: '#app',
 template: `
  <div id="#app">
   <keep-alive>
    <component :is="renderCom"></component>
   </keep-alive>
   <button @click="change">切换组件</button>
  </div>
 `,
 components: {
  Foo,
  Bar
 },
 data: {
  renderCom: 'Foo'
 },
 methods: {
  change () {
   this.renderCom = this.renderCom === 'Foo' ? 'Bar': 'Foo'
  }
 }
})

上面例子中,根实例的template会被编译成如下render函数

function anonymous(
) {
 with(this){return _c('div',{attrs:{"id":"#app"}},[_c('keep-alive',[_c(renderCom,{tag:"component"})],1),_c('button',{on:{"click":change}})],1)}
}

可使用在线编译:https://cn.vuejs.org/v2/guide/render-function.html#模板编译

根据上面的render函数可以知道,vnode生成的过程是深度递归的,先创建子元素的vnode再创建父元素的vnode。
所以首次渲染的时候,在生成keep-alive组件vnode的时候,Foo组件的vnode已经生成好了,并且作为keep-alive组件vnode构造函数(_c)的参数传入。

_c('keep-alive',[_c(renderCom,{tag:"component"})

生成的keep-alive组件的vnode如下

{
 tag: 'vue-component-2-keep-alive',
 ...
 children: undefined,
 componentInstance: undefined,
 componentOptions: {
  Ctor: f VueComponent(options),
  children: [Vnode],
  listeners: undefined,
  propsData: {},
  tag: 'keep-alive'
 },
 context: Vue {...}, // 调用 $createElement/_c的组件实例, 此处是根组件实例对象
 data: {
  hook: {
   init: f,
   prepatch: f,
   insert: f,
   destroy: f
  }
 }
}

此处需要注意组件的vnode是没有children的,而是将原本的children作为vnode的componentOptions的children属性,componentOptions在组件实例化的时候会被用到,同时在初始化的时候componentOptions.children最终会赋值给vm.$slots,源码部分如下

// createComponent函数
function createComponent (Ctor, data, context, children, tag) {
 // 此处省略部分代码
 ...
 var vnode = new VNode(
  ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
  data, undefined, undefined, undefined, context,
  { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
  asyncFactory
 );
 return vnode
}

Vue最后都会通过patch函数进行渲染,将vnode转换成真实的dom,对于组件则会通过createComponent进行渲染

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
 var i = vnode.data;
 if (isDef(i)) {
  var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
  if (isDef(i = i.hook) && isDef(i = i.init)) {
  i(vnode, false /* hydrating */);
  }
  // after calling the init hook, if the vnode is a child component
  // it should've created a child instance and mounted it. the child
  // component also has set the placeholder vnode's elm.
  // in that case we can just return the element and be done.
  if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue);
  insert(parentElm, vnode.elm, refElm);
  if (isTrue(isReactivated)) {
   reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  }
  return true
  }
 }
 }

接下去分两步介绍

  1. keep-alive组件本身的渲染
  2. keep-alive包裹组件的渲染,本例中的Foo组件和Bar组件

先讲讲本例中针对keep-alive组件本身的渲染

  1. 根组件实例化
  2. 根组件$mount
  3. 根组件调用mountComponent
  4. 根组件生成renderWatcher
  5. 根组件调用updateComponent
  6. 根组件调用vm.render()生成根组件vnode
  7. 根组件调用vm.update(vnode)
  8. 根组件调用vm.patch(oldVnode, vnode)
  9. 根组件调用createElm(vnode)
  10. 在children渲染的时候,如果遇到组件类型的vnode则调用createComponent(vnode),而正是在这个过程中,进行了子组件的实例化及挂载($mount)

所以在执行createElm(keepAliveVnode)的过程中会对keep-alive组件的实例化及挂载,而在实例化的过程中,keep-alive包裹的子组件的vnode会赋值给keep-alive组件实例的$slot属性,所以在keep-alive实例调用render函数时,可以通过this.$slot拿到包裹组件的vnode,在demo中,就是Foo组件的vnode,具体分析下keep-alive组件的render函数

render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
 if (componentOptions) {
  const name: ?string = getComponentName(componentOptions)
  const { include, exclude } = this
  if (
  // not included
  (include && (!name || !matches(include, name))) ||
  // excluded
  (exclude && name && matches(exclude, name))
  ) {
  return vnode
  }

  const { cache, keys } = this
  const key: ?string = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
  if (cache[key]) {
  vnode.componentInstance = cache[key].componentInstance
  remove(keys, key)
  keys.push(key)
  } else {
  cache[key] = vnode
  keys.push(key)
  if (this.max && keys.length > parseInt(this.max)) {
   pruneCacheEntry(cache, keys[0], keys, this._vnode)
  }
  }
  vnode.data.keepAlive = true
 }
 return vnode || (slot && slot[0])
 }

上面分析到,在执行createElm(keepAliveVnode)的过程中,会执行keep-alive组件的实例化及挂载($mount),而在挂载的过程中,会执行keep-alive的render函数,之前分析过,在render函数中,可以通过this.$slot获取到子组件的vnode,从上面源码中,可以知道,keep-alive只处理默认插槽的第一个子组件,言外之意如果在keep-alive中包裹多个组件的话,剩下的组件会被忽略,例如:

<keep-alive>
 <Foo />
 <Bar />
</keep-alive>
// 只会渲染Foo组件

继续分析,在拿到Foo组件vnode后,判断了componentOptions,由于我们的Foo是一个组件,所以这里componentOptions是存在的,进到if逻辑中,此处include 表示只有匹配的组件会被缓存,而 exclude 表示任何匹配的组件都不会被缓存,demo中并没有设置相关规则,此处先忽略。

const { cache, keys } = this
cache, keys是在keep-alive组件的create钩子中生成的,用来存储被keep-alive缓存的组件的实例以及对应vnode的key
created () {
 this.cache = Object.create(null)
 this.keys = []
}

继续下面

const key: ?string = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
if (cache[key]) {
 vnode.componentInstance = cache[key].componentInstance
 remove(keys, key)
 keys.push(key)
} else {
 cache[key] = vnode
 keys.push(key)
 if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
 }
}

首先,取出vnode的key,如果vnode.key存在则使用vnode.key,不存在则用componentOptions.Ctor.cid + (componentOptions.tag ?::${componentOptions.tag}: '')作为存储组件实例的key,据此可以知道,如果我们不指定组件的key的话,对于相同的组件会匹配到同一个缓存,这也是为什么最开始在描述keep-alive的时候强调它是一个组件级的缓存方案。

那么首次渲染的时候,cache和keys都是空的,这里就会走else逻辑

cache[key] = vnode
keys.push(key)
if (this.max && keys.length > parseInt(this.max)) {
 pruneCacheEntry(cache, keys[0], keys, this._vnode)
}

以key作为cache的健进行存储Foo组件vnode(注意此时vnode上面还没有componentInstance),这里利用了对象存储的原理,之后进行Foo组件实例化的时候会将其实例赋值给vnode.componentInstance,那么在下次keep-alive组件render的时候就可以获取到vnode.componentInstance。

所以首次渲染仅仅是在keep-alive的cache上面,存储了包裹组件Foo的vnode。

针对包裹组件的渲染

上面已经讲到执行了keep-alive的render函数,根据上面的源码可以知道,render函数返回了Foo组件的vnode,那么在keep-alive执行patch的时候,会创建Foo组件的实例,然后再进行Foo组件的挂载,这个过程与普通组件并没有区别,在此不累述。

当组件从Foo切换到Bar时

本例中由于renderCom属性的变化,会触发根组件的renderWatcher,之后会执行patch(oldVnode, vnode)
在进行child vnode比较的时候,keep-alive的新老vnode比较会被判定为sameVnode,之后会进入到patchVnode的逻辑

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
 if (oldVnode === vnode) {
  return
 }
 // 此处省略代码
 ...
 var i;
 var data = vnode.data;
 if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  i(oldVnode, vnode);
 }
 // 此处省略代码
 ...
}

由于我们的keep-alive是组件,所以在vnode创建的时候,会注入一些生命周期钩子,其中就包含prepatch钩子,其代码如下

prepatch: function prepatch (oldVnode, vnode) {
 var options = vnode.componentOptions;
 var child = vnode.componentInstance = oldVnode.componentInstance;
 updateChildComponent(
  child,
  options.propsData, // updated props
  options.listeners, // updated listeners
  vnode, // new parent vnode
  options.children // new children
 );
}

由此可知,keep-alive组件的实例在此次根组件重渲染的过程中会复用,这也保证了keep-alive组件实例上面之前存储cache还是存在的

var child = vnode.componentInstance = oldVnode.componentInstance;

下面的updateChildComponent这个函数非常关键,这个函数担任了Foo组件切换到Bar组件的关键任务。我们知道,由于keep-alive组件是在此处是复用的,所以不会再触发initRender,所以vm.$slot不会再次更新。所以在updateChildComponent函数担起了slot更新的重任

function updateChildComponent (
 vm,
 propsData,
 listeners,
 parentVnode,
 renderChildren
) {
 if (process.env.NODE_ENV !== 'production') {
 isUpdatingChildComponent = true;
 }

 // determine whether component has slot children
 // we need to do this before overwriting $options._renderChildren
 var hasChildren = !!(
 renderChildren ||    // has new static slots
 vm.$options._renderChildren || // has old static slots
 parentVnode.data.scopedSlots || // has new scoped slots
 vm.$scopedSlots !== emptyObject // has old scoped slots
 );

 // ...

 // resolve slots + force update if has children
 if (hasChildren) {
 vm.$slots = resolveSlots(renderChildren, parentVnode.context);
 vm.$forceUpdate();
 }

 if (process.env.NODE_ENV !== 'production') {
 isUpdatingChildComponent = false;
 }
}

updateChildComponent函数主要更新了当前组件实例上的一些属性,这里包括props,listeners,slots。我们着重讲一下slots更新,这里通过resolveSlots获取到最新的包裹组件的vnode,也就是demo中的Bar组件,之后通过vm.$forceUpdate强制keep-alive组件进行重新渲染。(小提示:当我们的组件有插槽的时候,该组件的父组件re-render时会触发该组件实例$fourceUpdate,这里会有性能损耗,因为不管数据变动是否对slot有影响,都会触发强制更新,根据vueConf上尤大的介绍,此问题在3.0会被优化),例如

// Home.vue
<template>
 <Artical>
  <Foo />
 </Artical>
</tempalte>

此例中当Home组件更新的时候,会触发Artical组件的强制刷新,而这种刷新是多余的。

继续,在更新了keep-alive实例的forceUpdate,之后再次进入到keep-alive的render函数中

render () {
 const slot = this.$slots.default
 const vnode: VNode = getFirstComponentChild(slot)
 // ...
}

此时render函数中获取到vnode就是Bar组件的vnode,接下去的流程和Foo渲染一样,只不过也是把Bar组件的vnode缓存到keep-alive实例的cache对象中。

当组件从Bar再次切换到Foo时

针对keep-alive组件逻辑还是和上面讲述的一样

  • 执行prepatch
  • 复用keep-alive组件实例
  • 执行updateChildComponent,更新$slots
  • 触发vm.$forceUpdate
  • 触发keep-alive组件render函数

再次进入到render函数,这时候cache[key]就会匹配到Foo组件首次渲染时候缓存的vnode了,看下这部分逻辑

const key: ?string = vnode.key == null
  ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  : vnode.key
if (cache[key]) {
 vnode.componentInstance = cache[key].componentInstance
 remove(keys, key)
 keys.push(key)
} else {
 cache[key] = vnode
 keys.push(key)
 if (this.max && keys.length > parseInt(this.max)) {
  pruneCacheEntry(cache, keys[0], keys, this._vnode)
 }
}

由于keep-alive包裹的组件是Foo组件,根据规则,此时生成的key和第一此渲染Foo组件时生成的key是一样的,所以本次keep-alive的render函数进入到了第一个if分支,也就是匹配到了cache[key],把缓存的componentInstance赋值给当前vnode,然后更新keys(当存在max的时候,能够保证被删除的是比较老的缓存)。

很多同学可能会问,这里设置vnode.componentInstance会有什么作用。这里涉及到vue的源码部分。

由于是从Bar组件切换到Foo组件,所以在patch的时候,比对到此处,并不会被判定为sameVnode,所以自然而然走到createElm,由于Foo是Vue组件,所以会进入到createComponent,所以最终进入到下面函数片段

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
 var i = vnode.data;
 if (isDef(i)) {
  var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
  if (isDef(i = i.hook) && isDef(i = i.init)) {
  i(vnode, false /* hydrating */);
  }
  // after calling the init hook, if the vnode is a child component
  // it should've created a child instance and mounted it. the child
  // component also has set the placeholder vnode's elm.
  // in that case we can just return the element and be done.
  if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue);
  insert(parentElm, vnode.elm, refElm);
  if (isTrue(isReactivated)) {
   reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
  }
  return true
  }
 }
 }

可以根据上面对于keep-alive源码的分析,此处isReactivated为true,接下去会进入到vnode生成的时候挂在的生命周期init函数

var componentVNodeHooks = {
 init: function init (vnode, hydrating) {
 if (
  vnode.componentInstance &&
  !vnode.componentInstance._isDestroyed &&
  vnode.data.keepAlive
 ) {
  // kept-alive components, treat as a patch
  var mountedNode = vnode; // work around flow
  componentVNodeHooks.prepatch(mountedNode, mountedNode);
 } else {
  var child = vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
  );
  child.$mount(hydrating ? vnode.elm : undefined, hydrating);
 }
 },
 prepatch: function prepatch (oldVnode, vnode) {
 var options = vnode.componentOptions;
 var child = vnode.componentInstance = oldVnode.componentInstance;
 updateChildComponent(
  child,
  options.propsData, // updated props
  options.listeners, // updated listeners
  vnode, // new parent vnode
  options.children // new children
 );
 },
 ...
}

此时由于实例已经存在,且keepAlive为true,所以会走第一个if逻辑,会执行prepatch,更新组件属性及一些监听器,如果存在插槽的话,还会更新插槽,并执行$forceUpdate,此处在前面已经分析过,不做累述。

继续createComponent,在函数内部会执行initComponent和insert

if (isDef(vnode.componentInstance)) {
 // 将实例上的dom赋值给vnode
 initComponent(vnode, insertedVnodeQueue);
 // 插入dom
 insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
 reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}

至此,当组件从Bar再次切换到Foo时,实例与dom都得到了复用,达到一个很高的体验效果!而我们之后要实现的feb-alive就是基于keep-alive实现的。

Vue页面级缓存解决方案feb-alive (下)

参考文档

vue-navigation
Vue.js 技术揭秘

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈Vue页面级缓存解决方案feb-alive (下)

    feb-alive github地址 体验链接 Vue页面级缓存解决方案feb-alive (上) 在剖析feb-alive实现之前,希望大家对以下基本知识有一定的了解. keep-alive实现原理 history api vue渲染原理 vue虚拟dom原理 feb-alive与keep-alive差异性 1. 针对activated钩子差异性 keep-alive配合vue-router在动态路由切换的情况下不会触发activated钩子,因为切换的时候组件没有变化,所以只能通过befor

  • 浅谈Vue页面级缓存解决方案feb-alive(上)

    feb-alive github地址 体验链接 使用理由 开发者无需因为动态路由或者普通路由的差异而将数据初始化逻辑写在不同的钩子里beforeRouteUpdate或者activated 开发者无需手动缓存页面状态,例如通过localStorage或者sessionStorage缓存当前页面的数据 feb-alive会帮你处理路由meta信息的存储与恢复 为什么开发feb-laive? 当我们通过Vue开发项目时候,是否会有以下场景需求? /a跳转到/b 后退到/a时候,希望从缓存中恢复页面

  • 浅谈Vue单页面做SEO的四种方案

    目录 1.Nuxt 服务端渲染应用部署 (SSR服务器渲染) 优势: 不足:(开发中遇到的坑) 2.Nuxt 静态应用部署 优势: 不足: 3.预渲染prerender-spa-plugin 优势: 不足: 4.Phantomjs 针对爬虫做处理 优势: 不足: 总结 众所周知,Vue SPA单页面应用对SEO不友好,当然也有相应的解决方案,通过查找资料,大概有以下4种方法.(本人只用过第一,第三种方案) 1.Nuxt 服务端渲染应用部署 (SSR服务器渲染) 关于服务器渲染:Vue官网介绍 ,

  • 浅谈vue同一页面中拥有两个表单时,的验证问题

    问题:如果vue的同一个页面拥有两个表单.验证第一个表单时没有通过就切换到第二个,那么第二个表单会出现验证错误的信息 我们可以通过为两个表单添加ref属性 之后在通过调用resetFields()方法来解决问题 代码如下 <el-form :model="form" :rules="rules" ref="form" label-width="100px"> this.$refs["form"]

  • 浅谈vue异步数据影响页面渲染

    今天遇到一个问题,要保证页面渲染前请求的数据已经得到了 由于user是在异步请求之后保存在session中,而在页面渲染时session中还没有user,页面直接报错. 因此我希望能在所有请求都得到后再去做页面的渲染. 1.先把id为app的div用v-if="appShow",定义appShow为false进行隐藏,避免渲染 2.写计数器,每1ms进行一次查询,如果session中已经有user,删除过滤器,移除滤布,appShow为true,开始渲染页面,这样可以保证页面的正常渲染

  • 浅谈vue单页面中有多个echarts图表时的公用代码写法

    html中: <div class="charts1"/> <div class="charts2"/> <div class="charts3"/> <div class="charts4"/> <div class="charts5"/> <div class="charts6"/> <div class=

  • 浅谈Django 页面缓存的cache_key是如何生成的

    页面缓存 e.g. @cache_page(time_out, key_prefix=key_prefix) def my_view(): ... 默认情况下,将使用配置中的default cache cache_page 装饰器是由缓存中间件 CacheMiddleware 转换而来的 CacheMiddleware 继承了 UpdateCacheMiddleware 和 FetchFromCacheMiddleware UpdateCacheMiddleware 继承自 Middleware

  • 浅谈uniapp页面跳转的解决方案

    目录 1.uniapp常用跳转API 2.微信小程序页面跳转API 3.其他页面跳转回tabbar页面的方法 4.页面来回跳转保持数据的方法 正常的页面跳转的api大家应该都清楚,但是涉及到多页面来回跳转以及返回到导航页的时候就需要一些技巧来进行处理,之前找了挺多文章也没有很详细的介绍,本文就详细说说页面跳转的那些事. 1.uniapp常用跳转API API 作用 uni.navigateTo 保留当前页面,跳转到应用内的某个页面,使用uni.navigateBack可以返回到原页面. uni.

  • 浅谈VUE项目打包后运行页面一片白问题

    目录 1.说明 2.问题说明 3.解决 3.1.index.js 3.2.utils.js 3.3.webpack.prod.conf.js 4.总结 1.说明 我们用VUE搭建一个脚手架后,在IDEA等工具中开发时,启动都没有什么问题,但是项目开发完成之后,可能需要部署上线,所以需要进行打包操作了,一般都是用下面命令进行打包: npm run build 打包过程一般没有什么问题,然后就会在工程目录下生成一个[dist]文件夹,里面就是我们打包好的文件,把这些文件部署到Nginx中或者Tomc

  • 浅谈Vue.js

    vue.js的总体评价"简单却不失优雅,小巧而不乏大匠" Vue.js简介 Vue.js的作者为Evan You(尤雨溪),任职于Google Creative Lab,虽然Vue是一个个人项目,但在发展前景上个人认为绝不输于Google的AngularJs,下面我会将Vue与Angular(Angular 1.0+版本)做一些简单的比较. Vue的主要特点就和它官网(http://cn.vuejs.org/)所介绍的那样: (1) 简洁 (2) 轻量 (3)快速 (4) 数据驱动 (

随机推荐