Vuex - 源码概览

栏目: JavaScript · 发布时间: 7年前

内容简介:对于这个

vuex 提供了一个 install 方法,用于给 vue.use 进行注册, install 方法对 vue 的版本做了一个判断, 1.x 版本和 2.x 版本的插件注册方法是不一样的:

// vuex/src/mixin.js
if (version >= 2) {
  Vue.mixin({ beforeCreate: vuexInit })
} else {
  // override init and inject vuex init procedure
  // for 1.x backwards compatibility.
  const _init = Vue.prototype._init
  Vue.prototype._init = function (options = {}) {
    options.init = options.init
      ? [vuexInit].concat(options.init)
      : vuexInit
    _init.call(this, options)
  }
}
复制代码

对于 1.x 版本,直接将 vuexInit 方法混入到 vueInit 方法中,当 vue 初始化的时候, vuex 也就随之初始化了 而对于 2.x 版本,则是通过混入 mixin 的方式,全局混入了一个 beforeCreated 钩子函数

这个 vuexInit 方法如下:

function vuexInit () {
  const options = this.$options
  // store injection
  if (options.store) {
    this.$store = typeof options.store === 'function'
      ? options.store()
      : options.store
  } else if (options.parent && options.parent.$store) {
    this.$store = options.parent.$store
  }
}
复制代码

目的很明确,就是把 options.store 保存在 this.$store 中, options 就是在 new 一个 vue 实例的时候,传入的参数集合对象,如果想使用 vuex 的话,肯定要把 store 传进来的,类似于下面的代码,所以可以拿到 options.store , this 指的是当前 Vue 实例,这个 options.store 就是 Store 对象的实例,所以可以在组件中通过 this.$store 访问到这个 Store 实例

const app = new Vue({
  el: '#app',
  // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
  store,
  components: { Counter },
  template: '<div class="app"></div>'
})
复制代码

Store

上面在 beforeCreate 生命周期中会拿到 options.store ,这个 store 自然也有初始化的过程

每一个 Vuex 应用的核心就是 store ,所以需要有 Store 的初始化过程,下面是一个最简单的 Store 示例(来源于vuex的官网):

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})
复制代码

Store 的源码位于 vuex/src/store.js ,在这个类的 constructor 中, new 了一个 vue 实例,所以 vuex 可以使用 vue 的很多特性,比如数据的响应式逻辑

初始化 Store 的过程中,其实也是对 modules 、 dispatch 、 commit 等进行了初始化操作

初始化module,构建module tree

在 Store 类的初始化函数 constructor 中,下面这句就是 modules 的初始化入口:

this._modules = new ModuleCollection(options)
复制代码

ModuleCollection 是一个 ES6 的类

// src/module/module-collection.js
constructor (rawRootModule) {
  // register root module (Vuex.Store options)
  this.register([], rawRootModule, false)
}
复制代码

这个类的 constructor 中调用了 register 方法,第二个参数 rawRootModule 就是 Store 初始化时传进来参数对象 options

// src/module/module-collection.js
register (path, rawModule, runtime = true) {
    // ...省略无关代码

    const newModule = new Module(rawModule, runtime)
    if (path.length === 0) {
      this.root = newModule
    } else {
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // register nested modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
复制代码

register 方法中会 new Module ,这个 Module 就是用来描述单个模块的类,里面定义了与单个模块相关( module )的数据结构( data struct )、属性以及方法,大概如下:

Vuex - 源码概览

这里面的方法、属性等,都和后续构建 Module Tree 有关

由于每个 module 都有其自己的 state 、 namespaced 、 actions 等,所以在初始化 module 的过程中,也会给每个 module 对象上挂载这些属性或方法,例如,下面就是挂载 state 的代码:

// src/module/module.js
this._rawModule = rawModule
const rawState = rawModule.state

// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
复制代码

开始初始化的时候,符合 path.length === 0 ,所以执行 this.root = newMudle ,接着遇到了 if (rawModule.modules) 这个判断语句,前面说了 rawModule 是传入的 options ,所以这里的 rawModule.modules 就是类似下面示例代码 ExampleA 中的 modules

/**
 * 示例代码 ExampleA
 */
const store = new Vuex.Store({
  // ... 省略无关代码
  modules: {
    profile: {
      state: { age: 18 },
      getters: {
        age (state) {
          return state.age
        }
      }
    },
    account: {
      namespaced: true,

      state: {
        isAdmin: true,
        isLogin: true
      },
      getters: {
        isAdmin (state) {
          return state.isAdmin
        }
      },
      actions: {
        login ({ state, commit, rootState }) {
          commit('goLogin')
        }
      },
      mutations: {
        goLogin (state) {
          state.isLogin = !state.isLogin
        }
      },

      // 嵌套模块
      modules: {
        // 进一步嵌套命名空间
        myCount: {
          namespaced: true,
          state: { count: 1 },
          getters: {
            count (state) {
              return state.count
            },
            countAddOne (state, getters, c, d) {
              console.log(123, state, getters, c, d);
              return store.getters.count
            }
          },
          actions: {
            addCount ({ commit }) {
              commit('addMutation')
            },
            delCount ({ commit }) {
              commit('delMutation')
            },
            changeCount ({ dispatch }, { type } = { type: 1 }) {
              if (type === 1) {
                dispatch('addCount')
              } else {
                dispatch('delCount')
              }
            }
          },
          mutations: {
            addMutation (state) {
              console.log('addMutation1');
              state.count = state.count + 1
            },
            delMutation (state) {
              state.count = state.count - 1
            }
          }
        },

        // 继承父模块的命名空间
        posts: {
          state: { popular: 2 },
          getters: {
            popular (state) {
              return state.popular
            }
          }
        }
      }
    }
  }
})
复制代码

所以这个判断是用于处理使用了 module 的情况,如果存在 modules ,则调用 forEachValue 对 modules 这个对象进行遍历处理

export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}
复制代码

拿到 modules 里面存在的所有 module ,进行 register 操作,这里面的 key 就是每个 module 的名称,例如 示例代码 ExampleA 中的 profile 、 account

到这里再次调用 this.register 方法的时候, path.length === 0 就不成立了,所以走 else 的逻辑,这里遇到了一个 this.get 方法:

// src/module/module-collection.js
get (path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}
复制代码

首先对 path 进行遍历,然后对遍历到的项调用 getChild ,这个 getChild 方法是前面 Module 类中的方法,用于根据 key ,也就是在当前模块中根据模块名获取子模块对象,与之对应的方法是 addChild ,是给当前模块添加一个子模块,也就是建立父子间的关联关系:

// src/module/module.js
this._children = Object.create(null)
// ...
addChild (key, module) {
  this._children[key] = module
}
// ...
getChild (key) {
  return this._children[key]
}
复制代码

看到这里应该就有点思路了,上述一系列操作实际上就是为了以模块名作为属性 key ,遍历所有模块及其子模块,构成一棵以 this.root 为顶点的 Modules Tree ,画成流程图的话会很清晰:

Vuex - 源码概览

安装 module tree

上面构建好一棵 module tree 之后,接下来就要 install 这棵树了

// src/store.js
const state = this._modules.root.state

installModule(this, state, [], this._modules.root)
复制代码

这个方法里做了很多事情,一个个看

首先是对命名空间 namespaced 的处理,如果发现当前 module 具有 namespaced 属性并且值为 true ,则会将其注册到 namespace map ,也就是存起来:

const namespace = store._modules.getNamespace(path)

// register in namespace map
if (module.namespaced) {
  store._modulesNamespaceMap[namespace] = module
}
复制代码

其中 getNamespace 方法就是 ModuleCollection 类上的方法,用于根据 path 拼接出当前模块的完整 namespace

getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}
复制代码

调用 getNamespace 获得命名空间名称,然后将命名空间名称作为 key ,将对应的命名空间所指的 module 对象作为 value 缓存到 store._modulesNamespaceMap 上,方便后续根据 namespace 查找模块,这个东西是可以通过 this.$store._modulesNamespaceMap 取到的,例如,对于 ExampleA 中的示例:

Vuex - 源码概览

接下来是一个判断逻辑,符合 !isRoot && !hot 条件才能执行,这里的 isRoot 是在 installModule 方法的开头定义的:

const isRoot = !path.length
复制代码

path 就是 module tree 维护的 module 父子关系的状态,当 path.length !== 0 时, isRoot 就是 true ,其实这里就是判断当前安装的模块是不是 root 模块,也就是 module tree 最顶层的节点,这个节点的 path.length 就是 0

由于 module 的安装,在 module tree 上就是从父级到子级,一开始执行 installModule 方法时,传入的 path 为 [] ,则 path.length === 0 ,所以会执行判断语句里面的代码

设置 state

// src/store.js
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state)
  })
}
复制代码

调用了 getNestedState :

function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}
复制代码

这里实际上是通过一层层 path.reduce 来查找最终的子模块的 state

例如,对于 account/myCount 下的 state 来说,它的 path 是 ['account', 'myCount'] ,全局 state 结构如下:

{
  profile: { ... },
  account: {
    isAdmin: true,
    isLogin: true,
    // 这是子模块 myCount的命名空间
    myCount: {
    // 这是子模块myCount的state
      count: 1
    },
    posts: {
      popular: 2
    }
  }
}
复制代码

当对这个全局 state 和 path = ['account', 'myCount'] 调用 getNestedState 方法时,最终将得到 /myCount 的 state :

{
  count: 1
}
复制代码

查找到具体子模块的 state 后,挂载到 store._withCommit 上,至于为什么挂到这上,这里暂且不分析,后面会说到

构建本地上下文

接下来会执行一个 makeLocalContext 方法:

const local = module.context = makeLocalContext(store, namespace, path)
复制代码

关于这个方法的作用,在它的注释上已经大概描述了一遍:

/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 */
function makeLocalContext (store, namespace, path) {
  // ...
}
复制代码

大概意思就是,本地化 dispatch 、 commit 、 getter 、 state ,如果(当前模块)没有 namespace ,则直接挂载到 root module 上

可能还是不太明白说的是什么意思,实际上,这就是对命名空间模块的一个处理,是为了在调用相应模块的 dispatch 、 commit 、 getters 以及 state 的时候,如果模块使用用了命名空间,则自动在路径上追加上 namespace

比如,对于 dispath 而言,如果当前模块存在 namespace ,则在调用这个模块的 dispatch 方法的时候,把 namespace 拼接到 type 上,然后根据这个拼接之后的 type 来查找 store 上的方法并执行:

// makeLocalContext
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
    const args = unifyObjectStyle(_type, _payload, _options)
    const { payload, options } = args
    let { type } = args

    if (!options || !options.root) {
      type = namespace + type
      if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
        console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
        return
      }
    }

    return store.dispatch(type, payload)
  }
复制代码

例如,对于 ExampleA 代码而言,想要改变 account/myCount 下的 count 值,可以直接全局调用 this.$store.dispatch('account/myCount/changeCount') , 当 type = 1 的时候又会执行 dispatch('addCount') ,这个 dispatch 其实是想要执行 account/myCount 模块下的 addCount 这个 actions ,而不是 root module 下的 addCount

于是,这里就进行了一个全路径 type 的拼接,将当前模块的 namespace 和 type 拼接到一起,即 account/myCount/ 与 addCount 的拼接,最后就拼接成了 account/myCount/addCount ,正是我们想要的 path ,最后将这个全路径 type 作为参数传给 store.dispatch 方法,这个过程主要是简化了嵌套 module 路径的拼接

commit 的逻辑与此类似,不过 getter 和 state 就有点不一样了

// src/store.js
// makeLocalContext

Object.defineProperties(local, {
  getters: {
    get: noNamespace
      ? () => store.getters
      : () => makeLocalGetters(store, namespace)
  },
  state: {
    get: () => getNestedState(store.state, path)
  }
})
复制代码

对于 getter ,如果没有 namspace 则直接返回 store.getters ,否则就调用 makeLocalGetters :

// src/store.js
function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    // skip if the target getter is not match this namespace
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}
复制代码

直接看这段代码可能不太清晰,所以这里带入一个例子看,比如对于 account/myCount/count 这个 getter 来说(即上述源码中的 type ),它的 namespace 就是 account/myCount/ ,它的 localType 就是 count ,当访问 gettersProxy.count 这个 getters 的时候,会自动指向全局的 account/myCount/count

然后是 state ,调用了 getNestedState ,这个方法前面已经说过了,作用和上面的大体一致,就不多说了

另外,这个过程中多次用到 Object.defineProperty 来设置给对象上的属性设置 get 函数,而不是直接给属性赋值,例如上面的 localType ,这种做法的目的在代码上也已经注释得很清楚了,就是为了能够做到在访问的时候才计算值,既减少了实时运算量,主要是又能够保证获取到的值是实时准确的,这个跟 vue 的响应式机制相关,这里就不多说了

综上, makeLocalContext 这个方法实际上就是做了一个具有命名空间的子模块的 dispatch 、 commit 、 getter 、 state 到全局的映射:

Vuex - 源码概览

vuex 的官网在介绍Actions 这一节的时候,有这么一段话:

Vuex - 源码概览

其中, Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象 这句话里的 context 对象指的就是这里本地化的 module 对象

注册 mutation action getter

Mutation

首先是 Mutation :

// src/store.js
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key
  registerMutation(store, namespacedType, mutation, local)
})
复制代码

这个 forEachMutation 方法是挂在 module 实例上的,这个方法没什么好说的,作用就是遍历当前 module 上的 mutations ,然后将这些 mutation 作为参数传入 registerMutation 方法中:

// src/store.js
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}
复制代码

该方法是给 root store 上的 _mutations[types] 添加 wrappedMutationHandler 方法(至于这个方法是干什么的是另外的问题,这里暂且不去看),而且 store._mutations[type] 的值是一个数组,也就是说同一个 type 的 _mutations 是可以对应多个 wrappedMutationHandler 方法的

例如,对于 ExampleA 中的 account/myCount 这个 module 来说,如果它的 namespaced 属性不存在,或者其值是 false ,即没有单独的命名空间,然后它的 mutations 中又有个叫 goLogin 的方法,这个方法在 account 这个 module 的 mutations 中同样存在,于是 state._mutations['account/goLogin'] 的数组中就存在了两项,一个是 account 下的 goLogin 方法,一个是 account/myCount 下的 goLogin 方法

而如果 account/myCount 的 namespaced 为 true ,就不存在这种情况了,因为这个时候,它的 goLogin 对应的 type 是 account/myCount/goLogin

action

// src/store.js
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key
  const handler = action.handler || action
  registerAction(store, type, handler, local)
})
复制代码

逻辑其实和上面的 Mutation 差不多,都是遍历所有的 actions ,然后挂到 store 的某个属性上,只不过 action 是挂到 store._actions 上,同样的,对于同一个 key ,也可以对应多个 action 方法,这也跟命名空间有关

getter

// src/store.js
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})
复制代码

getter 和上面的逻辑也都是差不多的,遍历所有的 getter ,然后挂到 store 的某个属性上,只不过 getter 是挂到 store._wrappedGetters 上,另外,对于同一个 key ,只允许存在一个值,如果存在多个值,则以第二个为准:

// src/store.js
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
复制代码

最后,如果当前模块具有子模块,则遍历其所有子模块,给这些子模块执行 installModule 方法,也就是把上面的步骤再次走一遍

至此, installModule 方法就执行完了,这里再回头整体看一遍, 调用 installModule 这个方法的时候,代码上面有两行注释:

// src/store.js

// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
复制代码

大概意思就是:

初始化 root module
同时也会递归地注册所有子 module
并且会将所有 module的 getters 收集到 this._wrappedGetters上
复制代码

经过上述的分析,再看这段注释,就没什么难以理解的了,这个方法( installModule )就是用于包括子模块在内的所有模块的 state、getters、actions、mutations 的一个初始化工作

初始化 store vm

接下来,又执行了 resetStoreVM :

// src/store.js

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)
复制代码

这个方法的作用可以从它的注释上大概看出来,初始化 store vm ,看到这个 vm 我们应该想到 vue 的实例 vm ,这里实际上就是让 store 借助 vue 的响应式机制

并且会将 _wrappedGetters 注册为 computed 的属性,也就是计算属性, _wrappedGetters 前面已经提到过了,就是各个模块的 getters 的集合,计算属性在 vue 中的特性之一是 计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值 ,也就是做到了 高效地实时计算 ,这里就是想让 store 上各个模块的 getters 也具备这种特性

// src/store.js
// resetStoreVM

store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
  // use computed to leverage its lazy-caching mechanism
  computed[key] = () => fn(store)
  Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true // for local getters
  })
})
复制代码

使用 forEachValue 来遍历 _wrappedGetters , forEachValue 前面也提到过了,所以这里的 fn(store) 实际上就是:

store._wrappedGetters[type] = function wrappedGetter (store) {
  return rawGetter(
    local.state, // local state
    local.getters, // local getters
    store.state, // root state
    store.getters // root getters
  )
}
复制代码

也就是 wrappedGetter 这个函数,返回一个 rawGetter 方法执行的结果,这里的 rawGetter 可以看作就是 getter 计算得到的结果,所以我们在 getter 方法的参数中拿到的四个参数指的就是上面四个:

// https://vuex.vuejs.org/zh/api/#getters

state,       // 如果在模块中定义则为模块的局部状态
getters,     // 等同于 store.getters
rootState    // 等同于 store.state
rootGetters  // 所有 getters
复制代码

拿到 getter 之后,就把它交给 computed

接下来又定义了一个 Object.defineProperty :

// src、store.js

Object.defineProperty(store.getters, key, {
  get: () => store._vm[key],
  enumerable: true // for local getters
})
复制代码

将 store.getters[key] 映射到 store._vm[key] 上,也就是当访问 store.getters[key] 的时候,就相当于获取 store._vm[key] 的计算值,至于这里的 store_vm 又是什么,跟下面的逻辑有关:

// src、store.js

store._vm = new Vue({
  data: {
    $$state: state
  },
  computed
})
复制代码

store._vm 实际上就是一个 vue 实例,这个实例只有 data 和 computed 属性,就是为了借助 vue 的响应式机制

这里实际上就是建立了一个 state 与 getter 的映射关系,因为 getter 的计算结果肯定依赖于 state 的,它们之间必然存在关联的关系, Store 类上有个 state 的访问器属性:

// src/store.js

get state () {
  return this._vm._data.$$state
}
复制代码

于是 state 到 getter 的映射关系流程如下:

Vuex - 源码概览

接下来是一个用于规范开发方式的逻辑:

// enable strict mode for new vm
if (store.strict) {
  enableStrictMode(store)
}
复制代码

store.strict 这里的 strict 是需要开发者在初始化 Store 的时候显式声明的,一般似乎大家都不怎么关心这个,不过为了更好地遵循 vuex 的开发规范,最好还是加上这个属性

enableStrictMode 方法如下:

// src/store.js

function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}
复制代码

上面说了, store._vm 其实就是一个 vue 实例,所以它有 $watch 方法,用于检测 this._data.$$state 的变化,也就是 state 的变化,当 state 变化的时候, store._committing 的值必须为 true

这个 store._committing 的值在 Store 的初始化代码中就已经定义了,值默认为 false :

// src/store.js

this._committing = false
复制代码

这个值的修改是在 _withCommit 方法中:

_withCommit (fn) {
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}
复制代码

确保在执行 fn 的时候, this._committing 值为 true ,然后执行完了再重置回去,这个 _withCommit 的执行场景一般都是对 state 进行修改,例如 commit :

// src/store.js

commit (_type, _payload, _options) {
  // 省略无关代码
  // ...
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  // 省略无关代码
  // ...
}
复制代码

enableStrictMode 主要就是为了防止不通过 vuex 提供的方法,例如 commit 、 replaceState 等,非法修改 state 值的情况,在开发环境下会报警告

总结

从上述分析来看, vuex 的初始化基本上与 store 的初始化紧密相关, store 初始化完毕, vuex 基本上也就初始化好了,不过过程中涉及到的部分还是比较多的

分析到现在,都是在说初始化, vuex 的 api 几乎没说上多少,而 vuex 的能力就是通过 api 来体现的,有空再分析下 vuex api 相关的吧


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

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

海量运维、运营规划之道

海量运维、运营规划之道

唐文 / 电子工业出版社 / 2014-1-1 / 59.00

《海量运维、运营规划之道》作者具有腾讯、百度等中国一线互联网公司多年从业经历,书中依托工作实践,以互联网海量产品质量、效率、成本为核心,从规划、速度、监控、告警、安全、管理、流程、预案、考核、设备、带宽等方面,结合大量案例与读者分享了作者对互联网海量运维、运营规划的体会。 《海量运维、运营规划之道》全面介绍大型互联网公司运维工作所涉及的各个方面,是每个互联网运维工程师、架构师、管理人员不可或......一起来看看 《海量运维、运营规划之道》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具