第一章:周介绍


  • 掌握脚手架发布模块的整体架构设计和实现原理
  • 掌握前端发布流程,并了解history和hash两种路由模式的区别
  • 深入理解vue-router原理

关键词
  • 前端发布
  • 前端路由
  • vue-router

第二章:脚手架发布流程架构设计


2-1 脚手架发布功能和流程讲解

  • 不依靠后端或服务端人员,使用脚手架快速对更改的内容进行项目发布。
  • imooc-cli —packagePath /Users/liumingzhou/Desktop/imooc-cli/packages/publish
    • git配置检查:保证远程仓库存在
    • git自动提交(输入commit信息):避免本地代码提交的繁杂操作
    • 云构建+云发布:检查build结果、按照依赖、云构建、云发布、云断开

点击查看【processon】

2-2 绘制项目发布架构设计图image.png

第三章: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. 'use strict';
  2. const Command = require('@cloudscope-cli/command')
  3. const log = require('@cloudscope-cli/log')
  4. class PublishCommand extends Command {
  5. init(){
  6. // 处理参数
  7. console.log('init',this._argv)
  8. }
  9. async exec(){
  10. try {
  11. const startTime = new Date().getTime()
  12. //1. 初始化检查
  13. this.prepare()
  14. //2.Git Flow自动化
  15. //3.云构建 + 云发布
  16. const endTime = new Date().getTime()
  17. log.info('本次发布耗时',Math.floor(endTime-startTime)/1000+'秒')
  18. } catch (e) {
  19. log.error(e.message)
  20. if(process.env.LOG_LEVEL === 'verbose'){
  21. log.error(e.message)
  22. }
  23. }
  24. }
  25. prepare(){
  26. }
  27. }
  28. function init(argv){
  29. return new PublishCommand(argv)
  30. }
  31. module.exports = init
  32. module.exports.PublishCommand = PublishCommand;

5-3 项目发布前预检查流程开发

结合上一节代码,本节主要内容为:

  • 初始化检查prepare
    • 确认项目是否npm项目
    • 确认项目的package.json中是否包含name/version/scripts/scripts.build
  1. prepare(){
  2. // 1.确认项目是否为npm项目
  3. const projectPath = process.cwd()
  4. const pkgPath = path.resolve(projectPath,'package.json')
  5. log.verbose('package.json',pkgPath)
  6. if(!fs.existsSync(pkgPath)){
  7. throw new Error('package.json不存在')
  8. }
  9. //2. 确认是否包含name\version\build命令
  10. const pkg = fse.readJsonSync(pkgPath)
  11. const {name,version,scripts} = pkg
  12. if(!name || !version || !scripts || !scripts.build ){
  13. throw new Error('package.json信息不全,请检查是否存在name、version和scripts(需提供build命令)')
  14. }
  15. this.projectInfo = {name,version,dir:projectPath}
  16. }

第6章 本周加餐:前端路由模式原理和 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. // src/router.js
  2. const {createWebHistory,createRouter} from 'vue-router',
  3. import Home from './pages/Home'
  4. import My from './pages/My'
  5. import Order from './pages/Order'
  6. const routes = [
  7. path:'/',name:'root',redirect:'/home'
  8. },{
  9. path:'/home',name:'home',component:Home
  10. },{
  11. path:'/my',name:'my',component:My
  12. },{
  13. path:'/order',name:'order',component:Order
  14. }]
  15. const routerHitory = createWebHistory()
  16. const router = createRouter({
  17. history:routerHitory,
  18. routes
  19. })
  20. export default router;
  21. // App.vue
  22. <template>
  23. <div id="vue3">vue3 template</div>
  24. <router-link to='/home'>Home | </router-link>
  25. <router-link to='/order'>Order | </router-link>
  26. <router-link to='/my'>My | </router-link>
  27. <router-view />
  28. </template>
  29. <script>
  30. export default {
  31. name: 'Vue3',
  32. }
  33. </script>
  34. <style>
  35. #vue3 {
  36. width: 100%;
  37. height: 100%;
  38. }
  39. </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. server {
  2. listen 8081;
  3. server_name resource;
  4. root /Users/liumingzhou/XXXXX/dist;
  5. autoindex on;
  6. location / {
  7. //跨域设置
  8. add_header Access-Control-Allow-Origin *;
  9. try_files: $uri $uri/ /index.html;
  10. }
  11. // 缓存设置
  12. add_header Cache-Control "no-cache, must-revalidate";
  13. }
  • 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的。

image.png

  • 项目启动之后,打开浏览器,点击刷新,会进入到调试处

image.png

  • 首先进入到createWebHistory方法中去(上图第21行代码),返回的routerHistory提供了一系列的工具方法(路由跳转、监听的事件方法等),具体实现源码以及注释如下:
  1. function createWebHistory(base) {
  2. // 传入的base进行处理
  3. base = normalizeBase(base);
  4. //historyNavigation提供了一些方法:location/push/replace/state
  5. // 该方法的实现浏览器URL变化但页面不刷新(push),核心是使用了浏览器对象模型history.pushState()和history.replaceState()方法。
  6. const historyNavigation = useHistoryStateNavigation(base);
  7. //生成一个listener:destory和listen方法
  8. const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
  9. function go(delta, triggerListeners = true) {
  10. if (!triggerListeners)
  11. historyListeners.pauseListeners();
  12. history.go(delta);
  13. }
  14. //将上面的事件拼装到一起,生成一个routerHistory对象返回
  15. const routerHistory = assign({
  16. // it's overridden right after
  17. location: '',
  18. base,
  19. go,
  20. createHref: createHref.bind(null, base),
  21. }, historyNavigation, historyListeners);
  22. Object.defineProperty(routerHistory, 'location', {
  23. enumerable: true,
  24. get: () => historyNavigation.location.value,
  25. });
  26. Object.defineProperty(routerHistory, 'state', {
  27. enumerable: true,
  28. get: () => historyNavigation.state.value,
  29. });
  30. return routerHistory;
  31. }

返回routerHistory对象后,接着进入到createRouter方法中,源码以及注释如下:

  1. // 从调用createRouter处,options中传入的参数为:history和routes
  2. function createRouter(options) {
  3. // 第一步生成matcher,matcher的作用是实现路由匹配
  4. // createRouterMatcher会为每一个简单或复杂的路由生成一个正则表达式
  5. const matcher = createRouterMatcher(options.routes, options);
  6. let parseQuery$1 = options.parseQuery || parseQuery;
  7. let stringifyQuery$1 = options.stringifyQuery || stringifyQuery;
  8. // 拿到history对象,是createWebHistory或者为createWebHashHistory
  9. let routerHistory = options.history;
  10. if ((process.env.NODE_ENV !== 'production') && !routerHistory)
  11. throw new Error('Provide the "history" option when calling "createRouter()":' +
  12. ' https://next.router.vuejs.org/api/#history.');
  13. //一些路由守卫的初始化、useCallbacks方法返回一个闭包。
  14. //每一个路由守卫都对应了一个闭包(代码就不贴了,主要返回了三个方法:add,list,reset,主要作用是缓存路由守卫)。
  15. const beforeGuards = useCallbacks();
  16. const beforeResolveGuards = useCallbacks();
  17. const afterGuards = useCallbacks();
  18. // 生成默认router
  19. const currentRoute = shallowRef(START_LOCATION_NORMALIZED);
  20. ………………
  21. // 一些初始化操作
  22. ………………
  23. // 这里的router即为最终的router对象,包含一系列的属性和方法
  24. const router = {
  25. currentRoute,
  26. addRoute,
  27. removeRoute,
  28. hasRoute,
  29. getRoutes,
  30. resolve,
  31. options,
  32. push,
  33. replace,
  34. go,
  35. back: () => go(-1),
  36. forward: () => go(1),
  37. beforeEach: beforeGuards.add,
  38. beforeResolve: beforeResolveGuards.add,
  39. afterEach: afterGuards.add,
  40. onError: errorHandlers.add,
  41. isReady,
  42. //此处的install方法是在执行app.user(router)的时候会执行到这里(即当这个router被返回到main.js后,下一步就会执行app.user(router),然后就会进入到这方法中去)
  43. install(app) {
  44. const router = this;
  45. //在此处注册了组件RouterLink和RouterView
  46. app.component('RouterLink', RouterLink);
  47. app.component('RouterView', RouterView);
  48. //全局主注册了$router $route
  49. app.config.globalProperties.$router = router;
  50. Object.defineProperty(app.config.globalProperties, '$route', {
  51. enumerable: true,
  52. get: () => unref(currentRoute),
  53. });
  54. if (isBrowser &&!started &&
  55. currentRoute.value === START_LOCATION_NORMALIZED) {
  56. started = true;
  57. //浏览器中push后,就会进行页面的渲染
  58. push(routerHistory.location).catch(err => {
  59. if ((process.env.NODE_ENV !== 'production'))
  60. warn('Unexpected error when starting the router:', err);
  61. });
  62. }
  63. const reactiveRoute = {};
  64. for (let key in START_LOCATION_NORMALIZED) {
  65. reactiveRoute[key] = computed(() => currentRoute.value[key]);
  66. }
  67. // 使用app.provide来做组件的传递
  68. // router-view和router-link中的参数是通过这里传递下去的
  69. // 关于provide的用法,见本节内容往下
  70. app.provide(routerKey, router);
  71. app.provide(routeLocationKey, reactive(reactiveRoute));
  72. app.provide(routerViewLocationKey, currentRoute);
  73. let unmountApp = app.unmount;
  74. installedApps.add(app);
  75. app.unmount = function () {
  76. installedApps.delete(app);
  77. if (installedApps.size < 1) {
  78. removeHistoryListener();
  79. currentRoute.value = START_LOCATION_NORMALIZED;
  80. started = false;
  81. ready = false;
  82. }
  83. unmountApp();
  84. };
  85. },
  86. };
  87. return router;

6-5 vue3高级特性:vue插件+provide跨组件通信

浏览器中如何实现URL变化但页面不刷新

  • 在控制台直接输入 history.pushState(null,null,’/Order’/),会发现浏览器窗口中地址发生了改变,但页面未刷新。
  • onpopState事件主要用来监听路由回退的操作。
  • 调试源码的步骤是,写一个click方法,点击debuger进行操作
  1. <button @click="jump">Jump</button>
  2. ………………
  3. <script>
  4. import { useRouter } from 'vue-router'
  5. export default {
  6. name: 'App',
  7. setup(){
  8. const router = useRouter();
  9. return{
  10. jump(){
  11. // eslint-disable-next-line no-debugger
  12. debugger
  13. router.push('/order')
  14. }
  15. }
  16. }
  17. }
  18. </script>

然后step into到router.push方法中,由此开始调试,进入pushWithRedirect()方法中(如下图) image.png 然后一步一步的,调试源码到最后,最终会通过history.pushState()方法,来改变地址而不发生页面的更新。

在上图的高亮部分resolve(to)是路由匹配的相关实现,下节继续。

6-7 vue-router路由匹配源码分析

我们输入路由后如何与我们自己定义的 routes中的路由进行匹配,就涉及到vue-router的核心概念 matcher。 两个关键点是:createRouter以及上一节提到的resollve方法。 image.png

本节重点讲解这个resolve方法,我们假定从 /home跳转到/order,代码以及注释如下:

  1. function resolve(rawLocation, currentLocation) {
  2. // 第一步是拿到currentLocation,即当前路由相关信息 【/home相关的】
  3. currentLocation = assign({}, currentLocation || currentRoute.value);
  4. // 判断传进来的路由‘/order’参数是不是string
  5. if (typeof rawLocation === 'string') {
  6. //进行一个形式的格式化吧
  7. let locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
  8. //最关键的一步是调用matcher下的resolve方法,传入两个参数 ‘/order’和‘/home’,到这里我们需要继续step into到这个方法中去调试。关键代码为: matchers.find(m => m.re.test(path));
  9. let matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
  10. let href = routerHistory.createHref(locationNormalized.fullPath);
  11. if ((process.env.NODE_ENV !== 'production')) {
  12. if (href.startsWith('//'))
  13. warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
  14. else if (!matchedRoute.matched.length) {
  15. warn(`No match found for location with path "${rawLocation}"`);
  16. }
  17. }
  18. // locationNormalized is always a new object
  19. return assign(locationNormalized, matchedRoute, {
  20. params: decodeParams(matchedRoute.params),
  21. hash: decode(locationNormalized.hash),
  22. redirectedFrom: undefined,
  23. href,
  24. });
  25. }
  26. …………………………………………
  27. }

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. import { defineComponent,h } from 'vue'
  2. // import Home from '../pages/Home';
  3. const TestComponent2 = defineComponent({
  4. name:'TestComponent2',
  5. props:{},
  6. setup(props, {slots} ){
  7. return ()=> {
  8. return h('div',{
  9. class:'test-component2',
  10. onClick(){
  11. alert('click')
  12. }
  13. },slots.default())
  14. }
  15. // return () =>{
  16. // return h(Home,{
  17. // onClick(){
  18. // alert('You Clicked the Home Component!')
  19. // }
  20. // })
  21. // }
  22. }
  23. })
  24. export default TestComponent2

6-10 深入解析router-view源码

  1. const RouterViewImpl = /*#__PURE__*/ defineComponent({
  2. name: 'RouterView',
  3. inheritAttrs: false,
  4. props: {
  5. name: {
  6. type: String,
  7. default: 'default',
  8. },
  9. route: Object,
  10. },
  11. // setup在整个组件初始化的时候只会执行一遍,但下面的render function,也就是40行的return部分会执行多次
  12. setup(props, { attrs, slots }) {
  13. (process.env.NODE_ENV !== 'production') && warnDeprecatedUsage();
  14. // injectedRoute决定router-view的刷新
  15. const injectedRoute = inject(routerViewLocationKey);
  16. // injectedRoute.value
  17. const routeToDisplay = computed(() => props.route || injectedRoute.value);
  18. const depth = inject(viewDepthKey, 0);
  19. const matchedRouteRef = computed(() => routeToDisplay.value.matched[depth]);
  20. provide(viewDepthKey, depth + 1);
  21. provide(matchedRouteKey, matchedRouteRef);
  22. provide(routerViewLocationKey, routeToDisplay);
  23. // 空的ref用来装载马上要渲染的view-router实例
  24. const viewRef = ref();
  25. watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
  26. if (to) {
  27. to.instances[name] = instance;
  28. if (from && from !== to && instance && instance === oldInstance) {
  29. if (!to.leaveGuards.size) {
  30. to.leaveGuards = from.leaveGuards;
  31. }
  32. if (!to.updateGuards.size) {
  33. to.updateGuards = from.updateGuards;
  34. }
  35. }
  36. }
  37. if (instance &&
  38. to &&
  39. (!from || !isSameRouteRecord(to, from) || !oldInstance)) {
  40. (to.enterCallbacks[name] || []).forEach(callback => callback(instance));
  41. }
  42. // 默认为pre属性,post在页面渲染之后执行 watch 监听
  43. }, { flush: 'post' });
  44. return () => {
  45. const route = routeToDisplay.value;
  46. const matchedRoute = matchedRouteRef.value;
  47. const ViewComponent = matchedRoute && matchedRoute.components[props.name];
  48. const currentName = props.name;
  49. if (!ViewComponent) {
  50. return normalizeSlot(slots.default, { Component: ViewComponent, route });
  51. }
  52. const routePropsOption = matchedRoute.props[props.name];
  53. const routeProps = routePropsOption
  54. ? routePropsOption === true
  55. ? route.params
  56. : typeof routePropsOption === 'function'
  57. ? routePropsOption(route)
  58. : routePropsOption
  59. : null;
  60. const onVnodeUnmounted = vnode => {
  61. // remove the instance reference to prevent leak
  62. if (vnode.component.isUnmounted) {
  63. matchedRoute.instances[currentName] = null;
  64. }
  65. };
  66. const component = h(ViewComponent, assign({}, routeProps, attrs, {
  67. onVnodeUnmounted,
  68. ref: viewRef,
  69. }));
  70. return (
  71. normalizeSlot(slots.default, { Component: component, route }) ||
  72. component);
  73. };
  74. },
  75. });