# 路由的导航守卫

别名:

  • 导航守卫
  • 路由守卫
  • 路由钩子
  • 路由拦截
  1. 作用: — 类似 【保安】

    • 守卫路由

        • 举例: 携带数据进
        • 举例: 事情完成才能出
  2. 导航守卫一共有三种形式 【 项目三选一 】

    • A: 全局导航守卫
      针对的整个项目,也就是管理或是监听整个项目

      1. 全局前置守卫 router.beforeEach(fn)

        1. fn 中有三个参数

          1. to: 目标路由
          2. from: 当前路由
          3. next: 它是一个拦截,表示是否允许通过
            1. true/false/’/login’/{ name: ‘login’}/ vm => {}
        2. 使用场景: 当我们本地存储/cookie 中有 token,那我们就自动跳转 /mine
      2. 全局的解析守卫
        1. 在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
        2. 必须保证整个项目的守卫和异步路由组件解析完成
      3. 全局的后置守卫 router.afterEach(to , from )
        • 可以做一些用户友好提示
    • B: 路由独享守卫 ·
      路由配置选项中的一个

      • 写在路由表中的守卫钩子
      • 针对的是和当前路由相关的,那么其他与之不相关的路由,它是无法监听它的变化情况的
      • 做路由拦截
        • 案例: 权限验证

          • 数据库: 用户组

            • 普通用户
            • 管理员
            • 超级管理员
          • 我们登录式,后台会返回给我们 info 信息,通过信息来判断它是哪个类型用户
    • C: 组件内守卫【 王者 】

      当前组件

      • 组件内的前置守卫 beforeRouteEnter((to,from,next)=>{})

        • 导航进入组件时,调用
        • this 是访问不到的,如果非要访问 this ,必须通过 next(vm=>{})访问
        • 因为组件此时没有创建,所以没有 this
        • 案例: 数据预载(进入组件前就获得数据)
          next(vm => { //vm 指的就是组件
          const result = JSON.parse(res.data.slice(7,-1)).rp_result.categorys
          vm.$set(vm.category,‘categorys’,result)
          })
      • 组件内的后置守卫
        • 当离开组件时,调用
        • this 是可以访问到
        • 案例: 注册/内容提交,用户交互提示
      • 组件内的更新守卫( 路由传参和路由的接参 )
        • 在当前路由改变,但是该组件被复用时调用
        • 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
        • 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
        • 可以访问组件实例 this
  3. 功能: 导航守卫可以监听路由变化情况,做路由跳转拦截

  4. 名词

    • 前置: 要进入当前路由 — 老师进入教室前
    • 后置: 要离开当前路由 — 老师离开教室
  5. 关于 next 的使用

    • next() 等价于 next( true ) 表示可以从当前路由跳转到目标路由
    • next( false ) 表示不通过, 表示从当前路由跳转不到目标路由
    • next(’/login’) 等价于 next({path:’/login’}) 跳转指定的路由
    • next(’/login’) 等价于 next({path:’/login’,params,query})
    • next( fn ) 数据预载
      next( vm => {})
  6. 业务: 当我们进入到一个项目的首页时,但是当我们没有注册账号时,它主动跳转到了注册/登录页

router.beforeEach((to, from, next) => {// next() // 默认true// next( false )// next()// 判断用户是否登录,我们找token值  cookie   localStorageconst token = localStorage.getItem("token");if (to.path === "/home") {next();}if (to.path == "/aa") {next();}if (to.path === "/mine/login") {if (token) {// 条件成立,那么我们就可以进行页面跳转了next("/home");} else {// 条件不成立,留在当前页面next();}} else {next(false);}
});

全局导航守卫案例

  1. 在 src > router >global-config.js 设置全局路由配置
import router from "./index";
// 书写全局导航守卫// 1. 全局导航守卫 - 前置守卫  -  管理的是路由的进入router.beforeEach((to, from, next) => {// to表示目标路径 - 我想进哪里// from表示当前路径 - 我现在所处的路径// next 表示 from  到  to 的连接 ,它可以控制 是否允许进入// 里面什么都不写表示不允许通过// next() 表示允许通过  默认实参为true// next() == next( true )// next( true )// next( false ) 表示不允许通过// next( false )/* 参数输出 */// console.log('to',to)// console.log('from',from)// 写一个自动的跳转// 当我们登陆了之后,我们前端要将后端给的token值存储起来,然后我们通过判断token是否存在,来进入项目const token = localStorage.getItem("token");// if ( token ) { //路由鉴权//     //表示token存在//     next()// } else {//     next( false )// }// 如果token不存在,那么我们自动跳转登录页面,然后让用户登录,来获得token值if (to.path == "/home") {next();}if (to.path == "/category") {next();}if (!token) {// next( 路由路径 )  可以跳转某一个路由/* 以下这么些会出现一个问题就是死循环 */// console.log( 1 )// next('/mine/login')// next()/*  所以要想不出现死循环,我们要加判断条件 思考: 需要打破这个死循环的条件 这样,用户进入哪一个页面可以由我们来控制 */switch (to.path) {case "/home":next("/mine/login");next();break;case "/category":next("/mine/login");next();break;case "/shopcar":next("/mine/login");next();break;default:break;}}next();
});// 2. 后置守卫 - 表示的离开router.afterEach((to, from) => {//没有next参数,那就没有拦截作用,它可以提示作用if (to.path == "/home") {alert("您是否要今天入首页?");}
});
router.beforeEach((to, from, next) => {// to and from are both route objects.must call `next`.// if (to.path === '/mine') return next()// const tokenStr = window.sessionStorage.getItem('token')// if (!tokenStr) return next('/mine')// next()const token = window.sessionStorage.getItem("token");if (!token) {if (to.path === "/home/zhy") {next("/mine");} else {next();}} else {next();}
});router.afterEach((to, from) => {// to and from are both route objects.if (to.path === "/recommend") {alert("hello");}
});
  1. 在 main.js 中引入全局路由配置,
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import { Button, Tabs, TabPane } from "element-ui";// Vue.component('el-button',Button)import axios from "axios";import "./router/global-config"; Vue.prototype.$http = axios; // 给Vue原型绑定 axios 这样我们项目中的每一个组件都可以通过 this.$http 来使用 axios了Vue.use(Button);
Vue.use(Tabs);
Vue.use(TabPane);Vue.config.productionTip = false;new Vue({router, // 在根实例选项中注入路由render: h => h(App)
}).$mount("#app");

路由独享守卫案例

import Home from "../pages/home";import Category from "../pages/category";import Recommend from "../pages/recommend";import ShopCar from "../pages/shopcar";import Mine from "../pages/mine";import Error from "../pages/error";import Zhy from "../pages/home/children/zhy.vue";import Lsy from "../pages/home/children/lsy.vue";import List from "../pages/list";
const token = window.localStorage.getItem("token");const routes = [{path: "", //路由路径redirect: "/home"},{path: "/home", //路由路径component: Home, //当前路由路径对应的组件children: [{path: "", //这里不写‘/’redirect: "zhy"},{path: "zhy", //这里不写‘/’component: Zhy,name: "zhy"},{path: "lsy",component: Lsy,name: "lsy",alias: "lishanyu"}]},{path: "/category", //路由路径component: Category //当前路由路径对应的组件},{path: "/recommend", //路由路径component: Recommend //当前路由路径对应的组件},{path: "/shopcar", //路由路径component: ShopCar, //当前路由路径对应的组件beforeEnter: (to, from, next) => {console.log("beforeEnter");if (token) {next();} else {next(false);}}},{path: "/mine", //路由路径component: Mine //当前路由路径对应的组件},{path: "/list/:id", //用于url携带id  和  查找字符串component: List,name: "list" //当前路由路径对应的组件},{path: "/error", //路由路径component: Error //当前路由路径对应的组件}// {//     path: '**', //错误路由匹配,vue规定这个必须放在最下面,它必须将上面的路由全找一遍,找不到才用当前这个//     redirect: '/error' //重定向// },
];// 导出模块export default routes;

组件内守卫【 王者 】案例
src > pages > home > children > zhy.vue

<template><article><div><p>张浩雨个人页面</p></div></article>
</template><script>
export default {name: "Zhy",beforeRouteLeave (to, from, next) {if(confirm('确定要离开么?')){next()}else{next(false)}}
};
</script><style>
</style>
<template><article><div><p>ShopCar页面</p><div>购物车<hr /><h3>数据预载的数据渲染</h3><ul><li v-for="item in categorys.data" :key="item.id"><p>商品名称: {{ item.shop_name }}</p><p>商品价格: {{ item.price }}</p></li></ul></div></div></article>
</template><script>import axios from "axios";export default {beforeRouteEnter(to, from, next) {axios.get("/mock/shopcar.json").then(res => {console.log(res);// 问题:我们想把获得的数据提前给到组件,但是我们在这个钩子中无法获得组件,this也没有// 解决next(vm => {console.log("张浩雨: beforeRouteEnter -> vm", vm);vm.$set(vm.categorys, "data", res.data);next();});}).catch(err => console.log(err));},name: "ShopCar",data() {return {categorys: {data: null}};}};
</script><style lang="stylus" scoped>ullist-style none
</style>
  • 全局导航守卫

    • router.beforeEach((to, from, next) => { })
    • router.afterEach((to, from) => { })
  • 路由独享守卫
    • beforeEnter ( to,from,next ) { }
  • 组内守卫
    • beforeRouteEnter (to, from, next) {
      next(vm => {
      // 通过 vm 访问组件实例
      })
      }
    • beforeRouteUpdate (to, from, next) {
      // just use this
      this.name = to.params.name
      next()
      }
    • beforeRouteLeave (to, from , next) {
      const answer = window.confirm(‘Do you really want to leave? you have unsaved changes!’)
      if (answer) {
      next()
      } else {
      next(false)
      }
      }

过渡动效transtion

1.下载animate.css $ yarn add animate.css -S

  1. main.js 引入animate.css
import Vue from 'vue'
import App from './App.vue';
import router from './router';
import axios from 'axios';
import {Button,TabPane,Tabs
} from 'element-ui';import './router/global-config';
import 'animate.css';Vue.prototype.$https = axios;
// Vue.use( ElementUI )
Vue.use(Button);
Vue.use(TabPane);
Vue.use(Tabs);Vue.config.productionTip = falsenew Vue({render: h => h(App),router //在根实例中注入路由
}).$mount('#app');
  1. 将router-view包裹起来
    <section class="row"><transitionmode="out-in"enter-active-class="animated rotateIn"leave-active-class="animated rotateOut"><!-- 动画元素 --><router-view></router-view></transition>

单个路由的过渡

上面的用法会给所有路由设置一样的过渡效果,如果你想让每个路由组件有各自的过渡效果,可以在各路由组件内使用 并设置不同的 name。

const Foo = {template: `<transition name="slide"><div class="foo">...</div></transition>`
}const Bar = {template: `<transition name="fade"><div class="bar">...</div></transition>`
}

基于路由的动态过渡

<!-- 使用动态的 transition name -->
<transition :name="transitionName"><router-view></router-view>
</transition>
// 接着在父组件内
// watch $route 决定使用哪种过渡
watch: {'$route' (to, from) {const toDepth = to.path.split('/').lengthconst fromDepth = from.path.split('/').lengththis.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'}
}

导航守卫 ----“导航”表示路由正在发生改变。

  • 正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

全局导航守卫

全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫:

const router = new VueRouter({ ... })router.beforeEach((to, from, next) => {// ...
})
  • 当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

  • 每个守卫方法接收三个参数:

    • to: Route: 即将要进入的目标 路由对象

    • from: Route: 当前导航正要离开的路由

    • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。

      • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
      • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
      • next(’/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
      • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

确保要调用 next 方法,否则钩子就不会被 resolved。

全局解析守卫 ----2.5.0 新增

  • 在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
  • 必须保证整个项目的守卫和异步路由组件解析完成

#全局后置钩子

  • 你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {// ...
});

路由独享守卫

  • 你可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({routes: [{path: "/foo",component: Foo,beforeEnter: (to, from, next) => {// ...}}]
});

这些守卫与全局前置守卫的方法参数是一样的。

组件内守卫

  • 你可以在路由组件内直接定义以下路由导航守卫:

    • beforeRouteEnter
    • beforeRouterUpdate
    • beforeRouterLeave
const Foo = {template: `...`,beforeRouteEnter(to, from, next) {// 在渲染该组件的对应路由被 confirm 前调用// 不!能!获取组件实例 `this`// 因为当守卫执行前,组件实例还没被创建},beforeRouteUpdate(to, from, next) {// 在当前路由改变,但是该组件被复用时调用// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。// 可以访问组件实例 `this`},beforeRouteLeave(to, from, next) {// 导航离开该组件的对应路由时调用// 可以访问组件实例 `this`}
};

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next 来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

beforeRouteEnter (to, from, next) {next(vm => {// 通过 `vm` 访问组件实例})
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdate 和 beforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

beforeRouteUpdate (to, from, next) {// just use `this`this.name = to.params.namenext()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

beforeRouteLeave (to, from , next) {const answer = window.confirm('Do you really want to leave? you have unsaved changes!')if (answer) {next()} else {next(false)}
}

完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。

结合 Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。
首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

const Foo = () => Promise.resolve({ /* 组件定义对象 */ })

第二,在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):

import('./Foo.vue') // 返回 Promise

注意

  • 如果您使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 可以正确地解析语法。

结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

const Foo = () => import('./Foo.vue')

在路由配置中什么都不需要改变,只需要像往常一样使用 Foo:

const router = new VueRouter({routes: [{ path: '/foo', component: Foo }]
})

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用 命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4)。

const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')

Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

路由元信息

定义路由的时候可以配置 meta 字段:

const router = new VueRouter({routes: [{path: '/foo',component: Foo,children: [{path: 'bar',component: Bar,// a meta fieldmeta: { requiresAuth: true }}]}]
})

那么如何访问这个 meta 字段呢?

首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录

例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。

一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。

下面例子展示在全局导航守卫中检查元字段:

router.beforeEach((to, from, next) => {if (to.matched.some(record => record.meta.requiresAuth)) {// this route requires auth, check if logged in// if not, redirect to login page.if (!auth.loggedIn()) {next({path: '/login',query: { redirect: to.fullPath }})} else {next()}} else {next() // 确保一定要调用 next()}
})

数据获取

  • 有时候,进入某个路由后,需要从服务器获取数据。例如,在渲染用户信息时,你需要从服务器获取用户的数据。我们可以通过两种方式来实现:

    • 导航完成之后获取:先完成导航,然后在接下来的组件生命周期钩子中获取数据。在数据获取期间显示“加载中”之类的指示。

    • 导航完成之前获取:导航完成前,在路由进入的守卫中获取数据,在数据获取成功后执行导航。

从技术角度讲,两种方式都不错 —— 就看你想要的用户体验是哪种。

导航完成后获取数据

路由的导航守卫过渡动效transtion导航守卫 路由懒加载 路由元信息 @stage3---wee2--day7相关推荐

  1. vue-router 的总结(导航守卫、组件过渡、组件间参数传输、懒加载)

    在实际项目中很多地方需要router的运用,下面从实践运用的例子总结一下需要引用router的地方 导航守卫:当用户没有登录时,限制用户不能访问某些页面 组件过渡,当页面中切换会出现动态的切换效果 组 ...

  2. 【亲测可用→防止入坑Routes】设置angular10项目异步加载、惰性加载、懒加载路由

    创建一个带路由的项目,依次执行下面每行代码 ng n RouingApp --routingcd RouingAppng g c components/firstng g c components/s ...

  3. 小程序看练代码02--模板、封装模板,include模板,wxs,全局样式,导入样式,微信基础样式库,hover-class等,过渡动画,空格使用,图片懒加载,ico图标,图像vh居中,矢量库,表单

    1.定义及使用模板 模板没有自己的js,也没有自己的监听,而是直接使用页面的js和监听.请更倾向去使用组件 <text>模板的使用</text> <!--模板的定义,要给 ...

  4. Vue—核心概念—异步组件和路由懒加载

    原文地址:Vue 异步组件&路由懒加载 目录 异步组件 异步组件介绍 异步组件声明 高级异步组件 路由懒加载 路由懒加载介绍 路由懒加载方法 把组件按组分块 异步组件 异步组件介绍 在开发大型 ...

  5. react.lazy 路由懒加载_React lazy/Suspense使用及源码解析

    React v16.6.0已经发布快一年了,为保障项目迭代发布,没有及时更新react版本,最近由于开启了新项目,于是使用新的react版本进行了项目开发.项目工程如何搭建,如何满足兼容性要求,如何规 ...

  6. Vue-Router + Webpack 路由懒加载实现

    一.前言 https://segmentfault.com/a/1190000015904599 当打包构建应用时,Javascript 包会变得非常大,影响页面加载.如果我们能把不同路由对应的组件分 ...

  7. 路由懒加载原理及使用

    懒加载解决的问题: 避免进入首页就加载全部的前端资源造成用户等待时间过长的问题. 就好比,访问 login 页面,你返回的 js 路由不仅有渲染 login 页面的,还有渲染 production 页 ...

  8. Vue 路由 过渡动效 数据获取

    过渡动效 <router-view> 是基本的动态组件,所以我们可以用 <transition> 组件给它添加一些过渡效果: <transition><rou ...

  9. Vue(小码哥王洪元)笔记06路由,url的hash,history,router-linke,路由跳转,动态路由,懒加载,路由嵌套,router参数传递,导航守卫

    1.什么是路由 路由器提供了两种机制:路由和传送 路由:数据报从来源到目的地的路径 传输:将输入端的数据转移到合适的输出端 路由有一个非常重要的概念教路由表 路由表本质上就是一个映射表,决定了数据包的 ...

最新文章

  1. RANSAC算法(2):(拟合平面)本文以地面为基础以及源码分布解读
  2. python代码实现二叉树的镜像树
  3. 在国内安装Pytorchy以及遇到的问题
  4. 步进电机正反转实验_电工基础:帮你学会电机正反转双重互锁控制
  5. CTFshow 信息收集 web12
  6. VC++基于APR实现禁止某个业务(开发行为控制软件用得着,例如上班禁止上QQ)...
  7. C++ reference很全面
  8. Html中value和name属性的作用
  9. Mysql客户端是不是jdbc_关于JDBC连接MySQL的问题,我一直解决不了(Myeclipse 环境下)...
  10. matlab 安装(2018a图解版)
  11. 微软开源用于大规模查找并修复漏洞的开发者工具 Project OneFuzz 框架
  12. [leetcode]831. 隐藏个人信息
  13. 开发人员常用的Oracle导入/导出命令
  14. 3h精通OpenCV(四)-绘制形状与文本
  15. Unity3D 场景编辑器扩展学习笔记-EditorWindow
  16. Cesium 之实现房屋模型拆解
  17. matlab生成面导出stl格式,导出建模文件到STL格式时需要注意的问题
  18. ceph rbd mysql_ceph-rbd使用
  19. 微信小程序(八)实战——加载图片images
  20. 推荐1个视频播放画中画工具!

热门文章

  1. 南卫理公会大学计算机科学,南卫理公会大学计算机科学研究生语言及申请要求-费用-课程设置...
  2. HTML打造动漫人物,百度贴吧打造二次元清明祭 回顾离开的动漫人物
  3. 获取post请求的几种常见方式
  4. Java如何创建支付接口
  5. python 图片数据清洗,图片去重,去掉模糊图片,去掉结构性相似的图片
  6. lol客户端打开之后是wegame并且卡住
  7. 十、生产者消费者问题
  8. 计算机为什么无法睡眠,电脑一休眠就唤醒不了该怎么解决
  9. C语言中输出26个字母
  10. html5 canvas创建弹性碰撞动画