Vue路由Hash模式分析

Vue-routerVue的核心组件,主要是作为Vue的路由管理器,Vue-router默认hash模式,即使用URLHash来模拟一个完整的URL,当URL改变时页面不会重新加载。

描述

Hash符号即#原本的目的是用来指示URL中指示网页中的位置,例如https://www.example.com/index.html#print即代表exampleindex.htmlprint位置,浏览器读取这个URL后,会自动将print位置滚动至可视区域,通常使用<a>标签的name属性或者<div>标签的id属性指定锚点。
通过window.location.hash属性能够读取锚点位置,可以为Hash的改变添加hashchange监听事件,每一次改变Hash,都会在浏览器的访问历史中增加一个记录,此外Hash虽然出现在URL中,但不会被包括在HTTP请求中,即#及之后的字符不会被发送到服务端进行资源或数据的请求,其是用来指导浏览器动作的,对服务器端没有效果,因此改变Hash不会重新加载页面。
Vue-router的作用就是通过改变URL,在不重新请求页面的情况下,更新页面视图,从而动态加载与销毁组件,简单的说就是,虽然地址栏的地址改变了,但是并不是一个全新的页面,而是之前的页面某些部分进行了修改,这也是SPA单页应用的特点,其所有的活动局限于一个Web页面中,非懒加载的页面仅在该Web页面初始化时加载相应的HTMLJavaScriptCSS文件,一旦页面加载完成,SPA不会进行页面的重新加载或跳转,而是利用JavaScript动态的变换HTML,默认Hash模式是通过锚点实现路由以及控制组件的显示与隐藏来实现类似于页面跳转的交互。

分析

Vue-router源码的实现比较复杂,会处理各种兼容问题与异常以及各种条件分支,文章分析比较核心的代码部分,精简过后的版本,重要部分做出注释,commit id560d11d

首先是在定义Router时调用Vue.use(VueRouter),此时会调用VueRouter类上的静态方法,即VueRouter.install = installinstall模块主要是保证Vue-router只被use一次,以及通过mixinVue的生命周期beforeCreate内注册实例,在destroyed内销毁实例,还有定义$router$route属性为只读属性以及<router-view><router-link>全局组件的注册。

// dev/src/install.js line 6
export function install (Vue) {if (install.installed && _Vue === Vue) returninstall.installed = true // 保证 Vue-router 只被 use 一次_Vue = Vueconst isDef = v => v !== undefinedconst registerInstance = (vm, callVal) => {let i = vm.$options._parentVnodeif (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {i(vm, callVal)}}Vue.mixin({beforeCreate () { // 注册实例if (isDef(this.$options.router)) { // this.$options.router 来自于 VueRouter 的实例化 // 判断实例是否已经挂载this._routerRoot = thisthis._router = this.$options.routerthis._router.init(this) // // 调用 VueRouter 的 init 方法Vue.util.defineReactive(this, '_route', this._router.history.current)} else {this._routerRoot = (this.$parent && this.$parent._routerRoot) || this // 将组件的 _routerRoot 都指向根 Vue 实例}registerInstance(this, this)},destroyed () { // 销毁实例 即挂载undefinedregisterInstance(this)}})Object.defineProperty(Vue.prototype, '$router', {get () { return this._routerRoot._router }})Object.defineProperty(Vue.prototype, '$route', {get () { return this._routerRoot._route }})Vue.component('RouterView', View) // 注册全局组件 <router-view>Vue.component('RouterLink', Link) // 注册全局组件 <router-link>const strats = Vue.config.optionMergeStrategies// use the same hook merging strategy for route hooksstrats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}

之后是VueRouter对象的构造函数,主要是先获取mode的值,如果mode的值为history但是浏览器不支持history模式,那么就强制设置mode值为hash,接下来根据mode的值,来选择vue-router使用哪种模式。

// dev/src/index.js line 40
constructor (options: RouterOptions = {}) {this.app = nullthis.apps = []this.options = optionsthis.beforeHooks = []this.resolveHooks = []this.afterHooks = []this.matcher = createMatcher(options.routes || [], this) // 创建路由匹配对象let mode = options.mode || 'hash'this.fallback =mode === 'history' && !supportsPushState && options.fallback !== false // 检车兼容if (this.fallback) {mode = 'hash'}if (!inBrowser) {mode = 'abstract'}this.mode = modeswitch (mode) {case 'history':this.history = new HTML5History(this, options.base)breakcase 'hash':this.history = new HashHistory(this, options.base, this.fallback) // 实例化Hash模式breakcase 'abstract':this.history = new AbstractHistory(this, options.base)breakdefault:if (process.env.NODE_ENV !== 'production') {assert(false, `invalid mode: ${mode}`)}}
}

在构造函数中调用了创建路由匹配对象的方法createMatcher,而在createMatcher中又调用了实际用以创建路由映射表的方法createRouteMap,可以说createMatcher函数的作用就是创建路由映射表,然后通过闭包的方式让addRoutesmatch函数能够使用路由映射表的几个对象,最后返回一个Matcher对象。

// dev/src/create-matcher.js line 16
export function createMatcher (routes: Array<RouteConfig>,router: VueRouter
): Matcher {const { pathList, pathMap, nameMap } = createRouteMap(routes) // 创建路由映射表function addRoutes (routes) {createRouteMap(routes, pathList, pathMap, nameMap)}function match ( // 路由匹配raw: RawLocation,currentRoute?: Route,redirectedFrom?: Location): Route {const location = normalizeLocation(raw, currentRoute, false, router) // location 是一个对象,类似于 {"_normalized":true,"path":"/","query":{},"hash":""}const { name } = locationif (name) { // 如果有路由名称 就进行nameMap映射const record = nameMap[name]  // nameMap[name] = 路由记录if (process.env.NODE_ENV !== 'production') {warn(record, `Route with name '${name}' does not exist`)}if (!record) return _createRoute(null, location)const paramNames = record.regex.keys.filter(key => !key.optional).map(key => key.name)if (typeof location.params !== 'object') {location.params = {}}if (currentRoute && typeof currentRoute.params === 'object') {for (const key in currentRoute.params) {if (!(key in location.params) && paramNames.indexOf(key) > -1) {location.params[key] = currentRoute.params[key]}}}location.path = fillParams(record.path, location.params, `named route "${name}"`)return _createRoute(record, location, redirectedFrom)} else if (location.path) { // 如果路由配置了path,到pathList和PathMap里匹配到路由记录 location.params = {}for (let i = 0; i < pathList.length; i++) {const path = pathList[i]const record = pathMap[path]if (matchRoute(record.regex, location.path, location.params)) {return _createRoute(record, location, redirectedFrom)}}}// no matchreturn _createRoute(null, location)}function redirect ( // 处理重定向record: RouteRecord,location: Location): Route {const originalRedirect = record.redirectlet redirect = typeof originalRedirect === 'function'? originalRedirect(createRoute(record, location, null, router)): originalRedirectif (typeof redirect === 'string') {redirect = { path: redirect }}if (!redirect || typeof redirect !== 'object') {if (process.env.NODE_ENV !== 'production') {warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)}return _createRoute(null, location)}const re: Object = redirectconst { name, path } = relet { query, hash, params } = locationquery = re.hasOwnProperty('query') ? re.query : queryhash = re.hasOwnProperty('hash') ? re.hash : hashparams = re.hasOwnProperty('params') ? re.params : paramsif (name) {// resolved named directconst targetRecord = nameMap[name]if (process.env.NODE_ENV !== 'production') {assert(targetRecord, `redirect failed: named route "${name}" not found.`)}return match({_normalized: true,name,query,hash,params}, undefined, location)} else if (path) {// 1. resolve relative redirectconst rawPath = resolveRecordPath(path, record)// 2. resolve paramsconst resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`)// 3. rematch with existing query and hashreturn match({_normalized: true,path: resolvedPath,query,hash}, undefined, location)} else {if (process.env.NODE_ENV !== 'production') {warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`)}return _createRoute(null, location)}}function alias ( // 处理别名record: RouteRecord,location: Location,matchAs: string): Route {const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`)const aliasedMatch = match({_normalized: true,path: aliasedPath})if (aliasedMatch) {const matched = aliasedMatch.matchedconst aliasedRecord = matched[matched.length - 1]location.params = aliasedMatch.paramsreturn _createRoute(aliasedRecord, location)}return _createRoute(null, location)}function _createRoute (  // 创建路由record: ?RouteRecord,location: Location,redirectedFrom?: Location): Route {if (record && record.redirect) {return redirect(record, redirectedFrom || location)}if (record && record.matchAs) {return alias(record, location, record.matchAs)}return createRoute(record, location, redirectedFrom, router) // 创建路由对象}return {match,addRoutes}
}// dev/src/create-route-map.js line 7
export function createRouteMap (routes: Array<RouteConfig>,oldPathList?: Array<string>,oldPathMap?: Dictionary<RouteRecord>,oldNameMap?: Dictionary<RouteRecord>
): {pathList: Array<string>,pathMap: Dictionary<RouteRecord>,nameMap: Dictionary<RouteRecord>
} {// the path list is used to control path matching priorityconst pathList: Array<string> = oldPathList || [] // 创建映射表// $flow-disable-lineconst pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)// $flow-disable-lineconst nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)routes.forEach(route => { // 遍历路由配置,为每个配置添加路由记录addRouteRecord(pathList, pathMap, nameMap, route)})// ensure wildcard routes are always at the endfor (let i = 0, l = pathList.length; i < l; i++) { // 确保通配符在最后if (pathList[i] === '*') {pathList.push(pathList.splice(i, 1)[0])l--i--}}if (process.env.NODE_ENV === 'development') {// warn if routes do not include leading slashesconst found = pathList// check for missing leading slash.filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')if (found.length > 0) {const pathNames = found.map(path => `- ${path}`).join('\n')warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)}}return {pathList,pathMap,nameMap}
}function addRouteRecord ( // 添加路由记录pathList: Array<string>,pathMap: Dictionary<RouteRecord>,nameMap: Dictionary<RouteRecord>,route: RouteConfig,parent?: RouteRecord,matchAs?: string
) {const { path, name } = route // 获得路由配置下的属性if (process.env.NODE_ENV !== 'production') {assert(path != null, `"path" is required in a route configuration.`)assert(typeof route.component !== 'string',`route config "component" for path: ${String(path || name)} cannot be a ` + `string id. Use an actual component instead.`)}const pathToRegexpOptions: PathToRegexpOptions =route.pathToRegexpOptions || {}const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)if (typeof route.caseSensitive === 'boolean') {pathToRegexpOptions.sensitive = route.caseSensitive}const record: RouteRecord = { // 生成记录对象path: normalizedPath,regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),components: route.components || { default: route.component },instances: {},name,parent,matchAs,redirect: route.redirect,beforeEnter: route.beforeEnter,meta: route.meta || {},props:route.props == null? {}: route.components? route.props: { default: route.props }}if (route.children) { // Warn if route is named, does not redirect and has a default child route.// If users navigate to this route by name, the default child will// not be rendered (GH Issue #629)if (process.env.NODE_ENV !== 'production') {if (route.name &&!route.redirect &&route.children.some(child => /^\/?$/.test(child.path))) {warn(false,`Named Route '${route.name}' has a default child route. ` +`When navigating to this named route (:to="{name: '${route.name}'"), ` +`the default child route will not be rendered. Remove the name from ` +`this route and use the name of the default child route for named ` +`links instead.`)}}route.children.forEach(child => { // 递归路由配置的 children 属性,添加路由记录const childMatchAs = matchAs? cleanPath(`${matchAs}/${child.path}`): undefinedaddRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)})}if (!pathMap[record.path]) { // 如果有多个相同的路径,只有第一个起作用,后面的会被忽略pathList.push(record.path)pathMap[record.path] = record}if (route.alias !== undefined) { // 如果路由有别名的话,给别名也添加路由记录const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]for (let i = 0; i < aliases.length; ++i) {const alias = aliases[i]if (process.env.NODE_ENV !== 'production' && alias === path) {warn(false,`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`)// skip in dev to make it workcontinue}const aliasRoute = {path: alias,children: route.children}addRouteRecord(pathList,pathMap,nameMap,aliasRoute,parent,record.path || '/' // matchAs)}}if (name) {if (!nameMap[name]) {nameMap[name] = record} else if (process.env.NODE_ENV !== 'production' && !matchAs) {warn(false,`Duplicate named routes definition: ` +`{ name: "${name}", path: "${record.path}" }`)}}
}

在上文的构造函数中实例化的HashHistory对象就是对于Hash模式下的路由的处理,主要是通过继承History对象以及自身实现的方法完成路由,以及针对于不支持history api的兼容处理,以及保证默认进入的时候对应的Hash值是以/开头的,如果不是则替换。在初始化VueRouter时调用的init方法调用了路由切换以及调用了setupListeners方法实现了路由的切换的监听回调,注意此时并没有在HashHistory对象的构造函数中直接添加事件监听,这是为了修复vuejs/vue-router#725的问题,简要来说就是说如果在beforeEnter这样的钩子函数中是异步的话,beforeEnter钩子就会被触发两次,原因是因为在初始化的时候如果此时的hash值不是以/开头的话就会补上#/,这个过程会触发hashchange事件,所以会再走一次生命周期钩子,也就意味着会再次调用beforeEnter钩子函数。

// dev/src/index.js line 21
export default class VueRouter {//...init (app: any /* Vue component instance */) {process.env.NODE_ENV !== 'production' &&assert(install.installed,`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +`before creating root instance.`)this.apps.push(app)// set up app destroyed handler// https://github.com/vuejs/vue-router/issues/2639app.$once('hook:destroyed', () => {// clean out app from this.apps array once destroyedconst index = this.apps.indexOf(app)if (index > -1) this.apps.splice(index, 1)// ensure we still have a main app or null if no apps// we do not release the router so it can be reusedif (this.app === app) this.app = this.apps[0] || nullif (!this.app) this.history.teardown()})// main app previously initialized// return as we don't need to set up new history listenerif (this.app) {return}this.app = appconst history = this.historyif (history instanceof HTML5History || history instanceof HashHistory) {const handleInitialScroll = routeOrError => {const from = history.currentconst expectScroll = this.options.scrollBehaviorconst supportsScroll = supportsPushState && expectScrollif (supportsScroll && 'fullPath' in routeOrError) {handleScroll(this, routeOrError, from, false)}}const setupListeners = routeOrError => {history.setupListeners() // 初始化添加事件监听handleInitialScroll(routeOrError)}history.transitionTo( // 如果默认页,需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由history.getCurrentLocation(),setupListeners,setupListeners)}history.listen(route => {this.apps.forEach(app => {app._route = route})})}//...
}// dev/src/history/base.js line 24
export class History {// ...transitionTo (location: RawLocation,onComplete?: Function,onAbort?: Function) {let route// catch redirect option https://github.com/vuejs/vue-router/issues/3201try {route = this.router.match(location, this.current) // // 获取匹配的路由信息} catch (e) {this.errorCbs.forEach(cb => {cb(e)})// Exception should still be thrownthrow e}const prev = this.currentthis.confirmTransition( // 确认跳转route,() => {this.updateRoute(route) // 更新当前 route 对象onComplete && onComplete(route)this.ensureURL() // 子类实现的更新url地址 对于 hash 模式的话 就是更新 hash 的值this.router.afterHooks.forEach(hook => {hook && hook(route, prev)})// fire ready cbs onceif (!this.ready) {this.ready = truethis.readyCbs.forEach(cb => {cb(route)})}},err => {if (onAbort) {onAbort(err)}if (err && !this.ready) {// Initial redirection should not mark the history as ready yet// because it's triggered by the redirection instead// https://github.com/vuejs/vue-router/issues/3225// https://github.com/vuejs/vue-router/issues/3331if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {this.ready = truethis.readyErrorCbs.forEach(cb => {cb(err)})}}})}confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {const current = this.currentthis.pending = routeconst abort = err => {// changed after adding errors with// https://github.com/vuejs/vue-router/pull/3047 before that change,// redirect and aborted navigation would produce an err == nullif (!isNavigationFailure(err) && isError(err)) {if (this.errorCbs.length) {this.errorCbs.forEach(cb => {cb(err)})} else {warn(false, 'uncaught error during route navigation:')console.error(err)}}onAbort && onAbort(err)}const lastRouteIndex = route.matched.length - 1const lastCurrentIndex = current.matched.length - 1if (isSameRoute(route, current) && // 如果是相同的路由就不跳转// in the case the route map has been dynamically appended tolastRouteIndex === lastCurrentIndex &&route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]) {this.ensureURL()return abort(createNavigationDuplicatedError(current, route))}const { updated, deactivated, activated } = resolveQueue( // 通过对比路由解析出可复用的组件,需要渲染的组件,失活的组件this.current.matched,route.matched)const queue: Array<?NavigationGuard> = [].concat( // 导航守卫数组// in-component leave guardsextractLeaveGuards(deactivated),  // 失活的组件钩子// global before hooksthis.router.beforeHooks, // 全局 beforeEach 钩子// in-component update hooksextractUpdateHooks(updated), // 在当前路由改变,但是该组件被复用时调用// in-config enter guardsactivated.map(m => m.beforeEnter), // 需要渲染组件 enter 守卫钩子// async componentsresolveAsyncComponents(activated) // 解析异步路由组件)const iterator = (hook: NavigationGuard, next) => {if (this.pending !== route) { // 路由不相等就不跳转路由return abort(createNavigationCancelledError(current, route))}try {hook(route, current, (to: any) => { // 只有执行了钩子函数中的next,才会继续执行下一个钩子函数,否则会暂停跳转,以下逻辑是在判断 next() 中的传参if (to === false) {// next(false) -> abort navigation, ensure current URLthis.ensureURL(true)abort(createNavigationAbortedError(current, route))} else if (isError(to)) {this.ensureURL(true)abort(to)} else if (typeof to === 'string' ||(typeof to === 'object' &&(typeof to.path === 'string' || typeof to.name === 'string'))) {// next('/') or next({ path: '/' }) -> redirectabort(createNavigationRedirectedError(current, route))if (typeof to === 'object' && to.replace) {this.replace(to)} else {this.push(to)}} else {// confirm transition and pass on the valuenext(to)}})} catch (e) {abort(e)}}// ...}// ...
}// dev/src/history/hash.js line 10
export class HashHistory extends History {constructor (router: Router, base: ?string, fallback: boolean) {super(router, base)// check history fallback deeplinkingif (fallback && checkFallback(this.base)) {return}ensureSlash()}// this is delayed until the app mounts// to avoid the hashchange listener being fired too earlysetupListeners () { // 初始化 这将延迟到mounts生命周期 以避免过早触发hashchange侦听器if (this.listeners.length > 0) {return}const router = this.routerconst expectScroll = router.options.scrollBehaviorconst supportsScroll = supportsPushState && expectScrollif (supportsScroll) {this.listeners.push(setupScroll())}const handleRoutingEvent = () => {const current = this.currentif (!ensureSlash()) {return}this.transitionTo(getHash(), route => {if (supportsScroll) {handleScroll(this.router, route, current, true)}if (!supportsPushState) {replaceHash(route.fullPath)}})}const eventType = supportsPushState ? 'popstate' : 'hashchange'window.addEventListener(eventType,handleRoutingEvent)this.listeners.push(() => {window.removeEventListener(eventType, handleRoutingEvent)})}push (location: RawLocation, onComplete?: Function, onAbort?: Function) {const { current: fromRoute } = thisthis.transitionTo(location,route => {pushHash(route.fullPath)handleScroll(this.router, route, fromRoute, false)onComplete && onComplete(route)},onAbort)}replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {const { current: fromRoute } = thisthis.transitionTo(location,route => {replaceHash(route.fullPath)handleScroll(this.router, route, fromRoute, false)onComplete && onComplete(route)},onAbort)}go (n: number) {window.history.go(n)}ensureURL (push?: boolean) {const current = this.current.fullPathif (getHash() !== current) {push ? pushHash(current) : replaceHash(current)}}getCurrentLocation () {return getHash()}
}

每日一题

https://github.com/WindrunnerMax/EveryDay

参考

https://router.vuejs.org/zh/
https://github.com/DDFE/DDFE-blog/issues/9
https://juejin.im/post/6844903647378145294
https://juejin.im/post/6844904062698127367
https://juejin.im/post/6844904018519523335
https://juejin.im/post/6844904012630720526
https://blog.csdn.net/zlingyun/article/details/83536589
https://ustbhuangyi.github.io/vue-analysis/v2/vue-router/install.html#vue-use
https://liyucang-git.github.io/2019/08/15/vue-router%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

Vue路由Hash模式分析相关推荐

  1. Vue路由History模式分析

    Vue路由History模式分析 Vue-router是Vue的核心组件,主要是作为Vue的路由管理器,Vue-router默认hash模式,通过引入Vue-router对象模块时配置mode属性可以 ...

  2. 解决vue路由hash模式下,微信网页授权问题

    解决vue路由hash模式下,微信网页授权问题 本人开发负责微信公众号端,菜单都是自定义菜单,然后每个菜单路径都是经过授权如:http://xxxx.com/ceshi/wechat/authoriz ...

  3. VueRouter — vue路由hash模式和history模式

    目录 一.前言 二.hash模式 三.history模式 一.前言 对于hash模式和history模式,最直接的区别就是地址栏带不带"#"号了. vue脚手架搭建的项目的路由默认 ...

  4. vue路由的两种模式:hash与history的区别

    前言:众所周知,vue-router有两种模式,hash模式和history模式,下面来看看两者的区别. 一.基本情况 直观区别:hash模式url带#号,history模式不带#号. 1.hash模 ...

  5. vue 一个页面多个router-view如何配置子路由_前端开发:如何安装配置Vue路由?

    大家好,我来了!本期为大家带来的Web前端学习知识是"前端开发:如何安装配置Vue路由?",喜欢Web前端的小伙伴,一起看看吧! Vue Router 是 Vue.js 官方的路由 ...

  6. vue 路由知识点梳理及应用场景整理

    最近做项目才发现,我确实对 vue-router 太不熟悉了,都只了解个简单用法就开始搞了,本来很简单的问题,都搞不清楚.现在重新看一遍文档,重新梳理一下. vue 路由的原理 说实话,路由我一直也就 ...

  7. 14.vue路由脚手架

    一.vue路由:https://router.vuejs.org/zh/ 1.定义 let router = new VueRouter({mode:"history/hash", ...

  8. vue路由1.0_【Vue】路由

    router 路由:切换不同的路径去展示不同的内容 安装路由 vue add router 是否使用历史模式 路由安装之后可以看到 src 文件夹中多了一个 views 文件夹,当中包含两个组件,还有 ...

  9. 关于vue路由模式导致微信jssdk授权问题的正确解决姿势

    如何正确解决vue路由模式weixin-jssdk授权问题 第一种情况:最常用的history模式 第二种也比较常用,hash模式 第三种就是memoryHistory 总结 第一种情况:最常用的hi ...

最新文章

  1. sqlldr导入时报少半个引号
  2. JS获取鼠标位置,兼容IE FF
  3. Java instanceof 关键字【复习】
  4. 饿了么超级会员数量暴增,外卖市场“去泡沫化”的先声?
  5. Connect Three
  6. 西南交通大学计算机程序设计实验13,西南交通大学C++实验报告.doc
  7. BeetleX网关之请求聚合
  8. ai电磁组属于什么组_RPA+AI 创新案例挑战赛 2020 【专业组】amp;【校园组】优胜名单来也!...
  9. s5pv210——I2C的代码实践
  10. Angular自定义管道(过滤器)方法
  11. 基于springboot网上购物商城系统设计与实现
  12. 联想小新13pro锐龙版网卡_诠释极致性价比 联想小新Pro 13标压锐龙版笔记本评测...
  13. 冰点还原精灵免费版下载
  14. 发票:企业级发票服务开放平台
  15. python项目(2)---xpath库的应用
  16. vivo S7e和华为nova8se 的区别 哪个好
  17. 软件需求说明及对应的测试用例,测试用例与需求的对应关系 - Mr.南柯 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  18. 字符串专题-LeetCode:剑指 Offer 58 - II. 左旋转字符串、LeetCode 459.重复的子字符串、 代码思路和注意点
  19. c++数独游戏3.3
  20. 项目需求 MVP与产品痛点

热门文章

  1. seata分布式事务一致性锁机制如何实现的
  2. IDEA配置码云Gitee的使用详解
  3. (四十九)java SpringCloud版本b2b2c鸿鹄云商平台全套解决方案
  4. Java注解学习一:注解术语
  5. Linux下如何查看tomcat是否启动
  6. (需要大神,请求解决,遇见runtime error 错误)poj 1009 java
  7. phpstorm编辑器乱码问题解决
  8. IDEA 类名下有红线解决方案:
  9. 关于proxy模式下,@Transactional标签在创建代理对象时的应用
  10. linux 背光驱动程序,Linux驱动工程师成长之路 LCD背光控制RT9379B