更新说明:对文章目录排版做了调整。
更新时间:2022-05-04
第一章:周介绍
- 掌握脚手架发布模块的整体架构设计和实现原理
- 掌握前端发布流程,并了解history和hash两种路由模式的区别
- 深入理解vue-router原理
关键词
第二章:脚手架发布流程架构设计
2-1 脚手架发布功能和流程讲解
- 不依靠后端或服务端人员,使用脚手架快速对更改的内容进行项目发布。
- imooc-cli --packagePath /Users/liumingzhou/Desktop/imooc-cli/packages/publish
- git配置检查:保证远程仓库存在
- git自动提交(输入commit信息):避免本地代码提交的繁杂操作
- 云构建+云发布:检查build结果、按照依赖、云构建、云发布、云断开
点击查看【processon】
2-2 绘制项目发布架构设计图
第三章:imooc-cli脚手架git flow 自动化架构设计
3-1 git flow 基础流程讲解
- git flow是2010年 Vincent Driessen设计出来的。
点击查看【processon】
3-2 git flow 多人协作流程讲解(详细讲解大厂git flow流程)
点击查看【processon】
3-3 脚手架git flow prepare阶段架构设计
ProcessOn画图
3-4 脚手架git flow 执行阶段架构设计 -Init
ProcessOn画图
第四章 imooc-cli 脚手架云构建 + 云发布架构设计
4-1 云构建+云发布整体流程设计
4-2 云构建+云发布详细流程设计1
4-3 云构建+云发布详细流程设计2
4-4 深入讲解云发布原理
点击查看【processon】
第五章:imooc-cli脚手架publish模块开发
5-1 创建publish模块
本模块在调试的时候出现问题:
- lerna create @cloudscope-cli/publish commands
- publish模块下lib的index中,打印日志:console.log(‘publish’)
- 接着使用webstorm调试exec的时候,debug没有进去。
- 参数为:
- Node parameters:/Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/core/cli/bin/index.js publish --targetPath /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/publish
- Working directory: ~/Desktop/test
查找原因为:
- 首先将本地连接全部去除 : 进入到node的modules目录下将相关的脚手架liugezhou的cloudscope的全部删除
- 进入到cloudscope-cli/core/cli 下npm install
- 发现在utils下等有一些package2的包,于是去到相关包下,删除重新安装
- npm install正确后,npm link,link完毕之后在本地which cloudscole-cli 看到有了包
- 然后在webstorm中调试的 Node parameters中重新配置(在publish之前加空格)
最后在一个空目录中输入以下命令进行调试:
cloudscope-cli publish --targetPath /Users/liumingzhou/Documents/imoocCourse/Web前端架构师/cloudscope-cli/commands/publish
打印出:publish
5-2 publish基本流程开发
接下来的重点就是编写业务代码:cloudscope-cli/commands/publish/lib/index.js
- 参考init中的代码,extends Command
- 必须实现init和exec方法,否则报错
- 该文件中用到的log / Command等需要npm install引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 'use strict'; const Command = require('@cloudscope-cli/command') const log = require('@cloudscope-cli/log') class PublishCommand extends Command { init(){ console.log('init',this._argv) }
async exec(){ try { const startTime = new Date().getTime() this.prepare()
const endTime = new Date().getTime() log.info('本次发布耗时',Math.floor(endTime-startTime)/1000+'秒') } catch (e) { log.error(e.message) if(process.env.LOG_LEVEL === 'verbose'){ log.error(e.message) } } } prepare(){ } }
function init(argv){ return new PublishCommand(argv) } module.exports = init module.exports.PublishCommand = PublishCommand;
|
5-3 项目发布前预检查流程开发
结合上一节代码,本节主要内容为:
- 初始化检查prepare
- 确认项目是否npm项目
- 确认项目的package.json中是否包含name/version/scripts/scripts.build
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| prepare(){ const projectPath = process.cwd() const pkgPath = path.resolve(projectPath,'package.json') log.verbose('package.json',pkgPath) if(!fs.existsSync(pkgPath)){ throw new Error('package.json不存在') } const pkg = fse.readJsonSync(pkgPath) const {name,version,scripts} = pkg if(!name || !version || !scripts || !scripts.build ){ throw new Error('package.json信息不全,请检查是否存在name、version和scripts(需提供build命令)') } this.projectInfo = {name,version,dir:projectPath} }
|
第六章 本周加餐:前端路由模式原理和 vue-router 源码讲解
本章内容测试代码上传至:https://github.com/liugezhou/vue-router-demo
6-1 vue-router-next完整运行流程解析
vue-router-next源码解析
vue-router常见问题:
- history和hash模式的区别是什么(涉及vue-router路由模式和前端发布原理)
- Vue dev模式下为什么不需要配置history fallback(涉及webpack-dev-server配置)
- 我们没有定义router-link和router-view,为什么代码里能直接使用(涉及vue-router初始化流程和Vue插件)
- 浏览器如何实现URL变化但页面不刷新(涉及vue-router history模式核心实现原理)
- vue-router如何实现路由匹配(涉及 vue-router Matcher 实现原理)
- router-view如何实现组件动态渲染?(涉及Vue动态组件)
通过imooc-cli脚手架安装一个vue3标准模版
- npm install -g @imooc-cli/core
- imooc-cli init test
- npm install -S vue-router(package.json中安装的版本为3.5.2,我们需要手动改成4.0.0-0,然后安装)
- 新建三个组件 src/pages/Home.vue | src/pages/Order.vue | src/pages/My.vue
- 新建src/router.js
- 并在main.js中引入,app.use(router)
- 在App.vue中使用和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| const {createWebHistory,createRouter} from 'vue-router', import Home from './pages/Home' import My from './pages/My' import Order from './pages/Order'
const routes = [ path:'/',name:'root',redirect:'/home' },{ path:'/home',name:'home',component:Home },{ path:'/my',name:'my',component:My },{ path:'/order',name:'order',component:Order }]
const routerHitory = createWebHistory() const router = createRouter({ history:routerHitory, routes })
export default router;
<template> <div id="vue3">vue3 template</div> <router-link to='/home'>Home | </router-link> <router-link to='/order'>Order | </router-link> <router-link to='/my'>My | </router-link> <router-view /> </template>
<script> export default { name: 'Vue3', } </script>
<style> #vue3 { width: 100%; height: 100%; } </style>
|
6-2 vue-router路由模式+history路由部署详细教学
Vue-router路由模式
- hash:createWebHashHistory()
- history:createWebHistory()
hash和history模式的区别
- 语法结构不同 :hash添加#意味着一个辅助说明,#后面参数发送改变后并不会加载资源,history模式只要路径改变就会重新请求资源,但是如果页面刷新的话 hash和history都是会重新加载资源的。
- 部署方式不同(history部署)
- npm run build
- nginx 静态网站服务器配置文件如下
- localhost:8081访问后,换不同的路由,页面刷新会显示404
- 此时根据Vue文档,Fallback,在nginx配置文件需要加入如下一行代码
- try_files: $uri $uri/ /index.html;
1 2 3 4 5 6 7 8 9 10 11 12 13
| server { listen 8081; server_name resource; root /Users/liumingzhou/XXXXX/dist; autoindex on; location / { add_header Access-Control-Allow-Origin *; try_files: $uri $uri/ /index.html; } add_header Cache-Control "no-cache, must-revalidate"; }
|
- SEO:hash不友好,实际开发应用为history模式。
- history模式跳转,利用的是浏览器对象中的history.pushState/replaceState/back/go/forward
- hash模式跳转,利用的是浏览器对象中的location.href
6-3 vue-cli源码调试+dev模式history fallback原理讲解
为什么Vue的dev模式下不需要配置history fallback?
- 说明:我们在dev模式下启动项目:npm run serve,在scripts中serve,实际执行的命令是 vue-cli-service serve,这个时候我们调试源码就在node_modules/.bin/vue-cli-service。如果执行全局 vue create,调试该命令的话我们就需要去本地全局安装的vue源码中去调试。
- 这个node_modules/.bin/vue-cli-service其实是link文件,我们通过 ll node_modules/.bin/vue-cli-service 就可以看出来。=》…/@vue/cli-service/bin/vue-cli-service.js
- 在webstorm中新建Node.js调试,Node parameters为:./node_modules/@vue/cli-service/bin/vue-cli-service.js serve
- 然后在上面的文件中打断点,开始进入debug调试模式。
- 跟着视频课程的调试,核心代码就是webpack的genHistoryApiFallbackRewrites 与try_files一样的作用
6-4 vue-router初始化过程源码分析
我们并没有定义router-link和router-view,为什么代码里能直接使用?
- 在vscode的router.js中添加debugger调试,没起作用,因此,该源码的调试是在webstorm中debug的。
- 项目启动之后,打开浏览器,点击刷新,会进入到调试处
- 首先进入到createWebHistory方法中去(上图第21行代码),返回的routerHistory提供了一系列的工具方法(路由跳转、监听的事件方法等),具体实现源码以及注释如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| function createWebHistory(base) { base = normalizeBase(base); const historyNavigation = useHistoryStateNavigation(base); const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace); function go(delta, triggerListeners = true) { if (!triggerListeners) historyListeners.pauseListeners(); history.go(delta); } const routerHistory = assign({ location: '', base, go, createHref: createHref.bind(null, base), }, historyNavigation, historyListeners); Object.defineProperty(routerHistory, 'location', { enumerable: true, get: () => historyNavigation.location.value, }); Object.defineProperty(routerHistory, 'state', { enumerable: true, get: () => historyNavigation.state.value, }); return routerHistory; }
|
返回routerHistory对象后,接着进入到createRouter方法中,源码以及注释如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| function createRouter(options) { const matcher = createRouterMatcher(options.routes, options); let parseQuery$1 = options.parseQuery || parseQuery; let stringifyQuery$1 = options.stringifyQuery || stringifyQuery; let routerHistory = options.history; if ((process.env.NODE_ENV !== 'production') && !routerHistory) throw new Error('Provide the "history" option when calling "createRouter()":' + ' https://next.router.vuejs.org/api/#history.'); const beforeGuards = useCallbacks(); const beforeResolveGuards = useCallbacks(); const afterGuards = useCallbacks(); const currentRoute = shallowRef(START_LOCATION_NORMALIZED);
………………
……………… const router = { currentRoute, addRoute, removeRoute, hasRoute, getRoutes, resolve, options, push, replace, go, back: () => go(-1), forward: () => go(1), beforeEach: beforeGuards.add, beforeResolve: beforeResolveGuards.add, afterEach: afterGuards.add, onError: errorHandlers.add, isReady, install(app) { const router = this; app.component('RouterLink', RouterLink); app.component('RouterView', RouterView); app.config.globalProperties.$router = router; Object.defineProperty(app.config.globalProperties, '$route', { enumerable: true, get: () => unref(currentRoute), }); if (isBrowser &&!started && currentRoute.value === START_LOCATION_NORMALIZED) { started = true; push(routerHistory.location).catch(err => { if ((process.env.NODE_ENV !== 'production')) warn('Unexpected error when starting the router:', err); }); } const reactiveRoute = {}; for (let key in START_LOCATION_NORMALIZED) { reactiveRoute[key] = computed(() => currentRoute.value[key]); } app.provide(routerKey, router); app.provide(routeLocationKey, reactive(reactiveRoute)); app.provide(routerViewLocationKey, currentRoute); let unmountApp = app.unmount; installedApps.add(app); app.unmount = function () { installedApps.delete(app); if (installedApps.size < 1) { removeHistoryListener(); currentRoute.value = START_LOCATION_NORMALIZED; started = false; ready = false; } unmountApp(); }; }, }; return router;
|
6-5 vue3高级特性:vue插件+provide跨组件通信
浏览器中如何实现URL变化但页面不刷新
- 在控制台直接输入 history.pushState(null,null,‘/Order’/),会发现浏览器窗口中地址发生了改变,但页面未刷新。
- onpopState事件主要用来监听路由回退的操作。
- 调试源码的步骤是,写一个click方法,点击debuger进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <button @click="jump">Jump</button> ……………… <script> import { useRouter } from 'vue-router' export default { name: 'App', setup(){ const router = useRouter(); return{ jump(){ debugger router.push('/order') } } } } </script>
|
然后step into到router.push方法中,由此开始调试,进入pushWithRedirect()方法中(如下图)
然后一步一步的,调试源码到最后,最终会通过history.pushState()方法,来改变地址而不发生页面的更新。
在上图的高亮部分resolve(to)是路由匹配的相关实现,下节继续。
6-7 vue-router路由匹配源码分析
我们输入路由后如何与我们自己定义的 routes中的路由进行匹配,就涉及到vue-router的核心概念 matcher。
两个关键点是:createRouter以及上一节提到的resollve方法。
本节重点讲解这个resolve方法,我们假定从 /home跳转到/order,代码以及注释如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function resolve(rawLocation, currentLocation) { currentLocation = assign({}, currentLocation || currentRoute.value); if (typeof rawLocation === 'string') { let locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path); let matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation); let href = routerHistory.createHref(locationNormalized.fullPath); if ((process.env.NODE_ENV !== 'production')) { if (href.startsWith('//')) warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`); else if (!matchedRoute.matched.length) { warn(`No match found for location with path "${rawLocation}"`); } } return assign(locationNormalized, matchedRoute, { params: decodeParams(matchedRoute.params), hash: decode(locationNormalized.hash), redirectedFrom: undefined, href, }); } ………………………………………… }
|
6-8 vue3新特性defineComponent讲解1 && 6-9 vue3新特性defineComponent讲解2
router-view如何实现组件动态渲染(涉及Vue动态组件)
- 本节从router对象的install方法开始,找到 app.component(‘RouterView’,RouterView)。
- 2328行定义:const RouterView = RouterViewImpl;
- RouterView就是RouterViewImpl方法,该方法源码如下
- 通过** 6-10 章节所示源码,我们看到router-view组件是以纯js实现的方式,使用defineComponent定义组件,组件的渲染使用了h**函数。
- 在进一步看源码之前,我们先来写个demo看 如何使用纯js方式编写组件。
- h 函数包含的三个参数为:dom标签、dom中需要绑定的一些属性、dom当中的children。
- 下面为代码演示,注释部分为直接使用Home组件的渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { defineComponent,h } from 'vue'
const TestComponent2 = defineComponent({ name:'TestComponent2', props:{}, setup(props, {slots} ){ return ()=> { return h('div',{ class:'test-component2', onClick(){ alert('click') } },slots.default()) } } })
export default TestComponent2
|
6-10 深入解析router-view源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| const RouterViewImpl = defineComponent({ name: 'RouterView', inheritAttrs: false, props: { name: { type: String, default: 'default', }, route: Object, }, setup(props, { attrs, slots }) { (process.env.NODE_ENV !== 'production') && warnDeprecatedUsage(); const injectedRoute = inject(routerViewLocationKey); const routeToDisplay = computed(() => props.route || injectedRoute.value); const depth = inject(viewDepthKey, 0); const matchedRouteRef = computed(() => routeToDisplay.value.matched[depth]); provide(viewDepthKey, depth + 1); provide(matchedRouteKey, matchedRouteRef); provide(routerViewLocationKey, routeToDisplay); const viewRef = ref(); watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => { if (to) { to.instances[name] = instance; if (from && from !== to && instance && instance === oldInstance) { if (!to.leaveGuards.size) { to.leaveGuards = from.leaveGuards; } if (!to.updateGuards.size) { to.updateGuards = from.updateGuards; } } } if (instance && to && (!from || !isSameRouteRecord(to, from) || !oldInstance)) { (to.enterCallbacks[name] || []).forEach(callback => callback(instance)); } }, { flush: 'post' }); return () => { const route = routeToDisplay.value; const matchedRoute = matchedRouteRef.value; const ViewComponent = matchedRoute && matchedRoute.components[props.name]; const currentName = props.name; if (!ViewComponent) { return normalizeSlot(slots.default, { Component: ViewComponent, route }); } const routePropsOption = matchedRoute.props[props.name]; const routeProps = routePropsOption ? routePropsOption === true ? route.params : typeof routePropsOption === 'function' ? routePropsOption(route) : routePropsOption : null; const onVnodeUnmounted = vnode => { if (vnode.component.isUnmounted) { matchedRoute.instances[currentName] = null; } }; const component = h(ViewComponent, assign({}, routeProps, attrs, { onVnodeUnmounted, ref: viewRef, })); return ( normalizeSlot(slots.default, { Component: component, route }) || component); }; }, });
|