• 摆脱DOM操作,从TodoMVC看angularJS


    取代jQuery?

    我很久之前便听说了angularJS的大名,之前的leader也经常感叹angularJS的设计如何如何精妙,可叹一直没有机会深入了解,国庆长假因为没钱出游,倒是可以对他做一个了解......

    根据之前的经验,就现有的前端项目,如果最初没有良好的设计,做到一定阶段一定会变得难以维护,就算最初有设计,变化无常的PM也会让你的项目BUG丛生。

    一个页面的复杂程度不断的增加,依赖模块也会变得混乱,而其中最为头疼的就是页面级随心所欲的DOM操作了!

    MVC类的框架可以很好的解决以上问题,而号称MVVM的angularJS在处理这种情况似乎更有话语权,所以我们今天便来好好研究其一番。

    angular适合做具有复杂数据交互的前端应用,他旨在让我们摆脱繁琐的DOM操作,而将注意力集中在业务逻辑上,这里摆脱繁琐的DOM操作是个非常关键的愿景,也是很多人不太理解,甚至会将jQuery这种库与Backbone或者angularJS这种框架做对比的原因。

    jQuery是非常优秀的DOM操作工具库,在DOM操作上,基本没有库能超越他了
    但Backbone&angularJS这种MVC是框架提供的是完整的解决方案,甚至会依赖jQuery&zepto,他们是两个东西,不能互相比较,所以完全没有angularJS要取代jQuery的可能,而当DOM操作过于杂乱一定是你的项目出了问题。

    这里举个jQuery不依赖MVC骨架的例子,我们的订单填写页,需要在商品数量变化后导致金额变化,并且没有选商品时,支付按钮不可点击:

    对于一个有些经验的菜鸟来说,可能会这样写代码:

    $('#reduceNum').click(function() {
          $('#payBar #num').text($('#curNum').html() - 1);
    });

    对于一些有一定经验的老鸟来说,可能会这样写代码:

    1 events: {
    2   'click #reduceNum': reduceNumAction  
    3 },
    4 
    5 reduceNumAction: function() {
    6   $('#payBar #num').text($('#curNum').html() - 1);
    7 }

    第一段代码可能会导致你年底加薪无望,并且在团队中没有话语权;而第二段代码积累到一定量后会让这个项目变得不可维护:

    ① 支付工具栏初始化状态如何显示,如果数字组件按需做异步加载,这个显示将变得更加负责。

    ② 哪些操作将导致支付栏变化,你如何组织这些变化的代码,是让他四散到各处,还是集合在一起,集合后导致函数过大怎么办?

    ③ 新增的导致工具栏变化的操作会不会对原来的操作造成影响,新增的代码放在何处?

    ④ 如果有地方要使用工具栏处的信息,取的信息会不会是无效的(取的时候可能正在变化),应该通过DOM取还是内存取?

    ⑤ 如果支付栏DOM结构如果变化,对你的程序影响有多大,如何主流程的影响,比较支付点击后只需要操作数据,不需要关注DOM?

    ⑥ ......

    这个就是仅仅依赖jQuery要面临的问题,并且这种问题是无解的,因为这里的专注点是DOM操作而不是数据,如果将关注点变成了数据,代码就不是这样写的,DOM操作仅仅是过程而不是目的,我们代码的目的,往往是展示数据、获取数据,这点一定要清晰。

    所以让我们带着这些问题:angular的优势在何处,他如何改善我们的编程体验,进入今天的学习吧。

    初探angularJS

    Hello World

    学习任何一门语言,Hello world是必不可少的,他是我们迈向精通的唯一路径:

    1 <!doctype html>
    2 <html ng-app>
    3 <head>
    4   <script src="angular.js" type="text/javascript"></script>
    5 </head>
    6 <body>
    7   Hello {{'World'}}!
    8 </body>
    9 </html>

    被{{}}包裹的便是angularJS变量,上述程序稍作改变的话:

     1 <!doctype html>
     2 <html ng-app>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body>
     7   <input ng-model="name" type="text" />
     8   Hello {{name}}!
     9 </body>
    10 </html>

    便会同步显示文本框输入内容,这里通信的基础是model对应着ng-model,只要被ng-app包裹就会受angularJS控制,用angularJS自己的话说:HTML标签增强

    作用域

    为什么文本框中的变化会体现在外层,这个涉及到了ng-model的双向绑定知识,我们暂时不予理睬,但是外层又是从哪里读取name这个变量的呢?

    在angular中,属性会存储在一个@scope(作用域)的对象上,每次我们对文本框的更新皆会通知$scope上的name属性,在angular中,$scope是连接controllers(控制器)与template(视图)的主要胶合器。

    上述代码完全不涉及js代码,真实的场景中每个代码段会对controller做依赖,我们这里对代码做一些更改:

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body ng-app="app" ng-controller="MainCtrl">
     7   <h1 ng-click="click()">
     8     Hello {{name}}!
     9   </h1>
    10   <script>
    11     var app = angular.module('app', []);
    12     app.controller('MainCtrl', function ($scope) {
    13       $scope.name = 'World';
    14       $scope.click = function () {
    15         $scope.name = '霹雳布袋戏';
    16       };
    17     });
    18   </script>
    19 </body>
    20 </html>

    这里首先定义了一个application模块,后续会看见,我们每次代码一定会新建一个application,相当于命名空间的意思,后面还可以做依赖用。

    接着,我们创建了一个controller模块,这里已经有点MVC的味道了,controller接受$scope属性,这个时候模板上所有子标签对这个控制器中的属性便有了访问权限,这里用到了一些angular指令

    ng-app:告诉html标签已经处于angular的控制了,可以使用angular的特性
    ng-controller:一个module下面可以包括多个控制器,每一个标签所属的控制器由该指令指定

    上述代码是将控制器中的数据读出来,我们同样也可以将View中的数据读入到控制器:

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body ng-app="app" ng-controller="MainCtrl">
     7   <input type="text" ng-model="message" />
     8   <h1 ng-click="click()">
     9     Hello {{name}}!
    10   </h1>
    11   <script>
    12     var app = angular.module('app', []);
    13     app.controller('MainCtrl', function ($scope) {
    14       $scope.name = 'World';
    15       $scope.click = function () {
    16         $scope.name = $scope.message;
    17       };
    18     });
    19   </script>
    20 </body>
    21 </html>

    PS:看到这里,老夫虎躯为之一振,对该特性的实现产生了兴趣,后续值得深入

    指令

    指令让我们有能力使用angular规定的方式为HTML标签增加新特性,angular内置了很多有用的指令,这里仍然举一个简单的例子说明问题:

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body ng-app="app">
     7   <ul ng-controller="MainCtrl">
     8     <li ng-repeat="v in arr">{{v}}</li>
     9   </ul>
    10   <script>
    11     var app = angular.module('app', []);
    12     app.controller('MainCtrl', function ($scope) {
    13       $scope.arr = ['素还真', '一页书', '叶小钗']
    14     });
    15   </script>
    16 </body>
    17 </html>

    我们除了使用angular的内置指令外,还可以自定义指令,比如这里的让文本框自动获取焦点的指令:

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body ng-app="app" ng-controller="MainCtrl">
     7   <input type="text" focus  ng-model="user.name" />
     8   <button ng-click="greet()">
     9     Click here!</button>
    10   <h3>
    11     {{ message }}</h3>
    12   <script>
    13     var app = angular.module('app', []);
    14     app.controller('MainCtrl', function ($scope) {
    15       $scope.greet = function () {
    16         $scope.message = "Hello, " + $scope.user.name;
    17       }
    18     });
    19     app.directive('focus', function () {
    20       return {
    21         link: function (scope, element, attrs) {
    22           element[0].focus();
    23         }
    24       };
    25     });
    26   </script>
    27 </body>
    28 </html>

    指令的使用可以很复杂,后续我们会更加深入,这里再举一个单独使用的例子:

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body ng-app="app">
     7   <hello></hello>
     8   <script>
     9     var app = angular.module('app', []);
    10     app.directive('hello', function () {
    11       return {
    12         restrict: "E",
    13         replace: true,
    14         template: "<div>显示固定数据,类似自定义标签</div>"
    15       }
    16     });
    17   </script>
    18 </body>
    19 </html>

    指令的定义有很多参数,可以指定该指令作为属性还是作为标签,这个我们后续再深入了解。

    过滤器

    感觉过滤器是参考的smarty的语法,一般而言是用作显示的增强,angular本身也提供了很多内置过滤器,比如:

    1 {{ "aaaa" | uppercase }} // AAAA
    2 {{ "BBBB" | lowercase }} // bbbb

    感觉比较有用的是日期操作过滤器:

    {{ 1427345339072 | date:'yyyy' }} // 2015
    {{ 1427345339072 |date:'MM' }} // 03
    {{ 1427345339072 | date:'d' }} // 26,一月中第多少天
    ......

    数字格式化:

    {{12.13534|number:2}} // 12.14 四舍五入保留两位小数
    {{10000000|number}} // 10,000,000

    当然,我们可以使用自定义过滤器,比如这里我想对超出某一区间的数字加...

     1 <!doctype html>
     2 <html>
     3 <head>
     4   <script src="angular.js" type="text/javascript"></script>
     5 </head>
     6 <body ng-app="app" ng-controller="MainCtrl">
     7   <input type="text" ng-model="message" />
     8   <h3>
     9     {{ message |myFilter }}</h3>
    10   <script>
    11     var app = angular.module('app', []);
    12     app.controller('MainCtrl', function ($scope) {
    13       $scope.message = '';
    14     });
    15 
    16     app.filter('myFilter', function () {
    17       return function (input, param) {
    18         return input.length < 5 ? input : input.substring(0, 5) + '...'
    19       }
    20     });
    21   </script>
    22 </body>
    23 </html>

    具备了以上知识,我们尝试进入To都MVC看看

    参考:http://www.cnblogs.com/whitewolf/p/angularjs-start.html

    TodoMVC

    我们由最新的TodoMVC下载代码:http://todomvc.com/,首先查看js引用情况:

    1 <script src="node_modules/angular/angular.js"></script>
    2 <script src="node_modules/angular-route/angular-route.js"></script>
    3 <script src="js/app.js"></script>
    4 <script src="js/controllers/todoCtrl.js"></script>
    5 <script src="js/services/todoStorage.js"></script>
    6 <script src="js/directives/todoFocus.js"></script>
    7 <script src="js/directives/todoEscape.js"></script>

    除了angular本体文件外,还多了个angular的扩展,做单页应用的路由功能的,这个路由代码量不大,使用和Backbone的路由比较类似;app.js为入口文件,配置路由的地方;余下是控制器文件文件以及一个localstorage的操作服务,余下就是指令了。

    代码首先定义了一个模块作为本次程序的命名空间:

    1 angular.module('todomvc', ['ngRoute'])

    ngRoute为其依赖项,可以从route的定义看出:

    1 var ngRouteModule = angular.module('ngRoute', ['ng']).
    2                         provider('$route', $RouteProvider),
    3     $routeMinErr = angular.$$minErr('ngRoute');

    这里来看看其router的配置,以及index.html的写法:

     1 <!doctype html>
     2 <html lang="en" data-framework="angularjs">
     3     <head>
     4         <meta charset="utf-8">
     5         <title>AngularJS • TodoMVC</title>
     6         <link rel="stylesheet" href="node_modules/todomvc-common/base.css">
     7         <link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
     8         <style>[ng-cloak] { display: none; }</style>
     9     </head>
    10     <body ng-app="todomvc">
    11         <ng-view />
    12 
    13         <script type="text/ng-template" id="todomvc-index.html">
    14             <section id="todoapp">
    15                 <header id="header">
    16                     <h1>todos</h1>
    17                     <form id="todo-form" ng-submit="addTodo()">
    18                         <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus>
    19                     </form>
    20                 </header>
    21                 <section id="main" ng-show="todos.length" ng-cloak>
    22                     <input id="toggle-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)">
    23                     <label for="toggle-all">Mark all as complete</label>
    24                     <ul id="todo-list">
    25                         <li ng-repeat="todo in todos | filter:statusFilter track by $index" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
    26                             <div class="view">
    27                                 <input class="toggle" type="checkbox" ng-model="todo.completed" ng-change="toggleCompleted(todo)">
    28                                 <label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
    29                                 <button class="destroy" ng-click="removeTodo(todo)"></button>
    30                             </div>
    31                             <form ng-submit="saveEdits(todo, 'submit')">
    32                                 <input class="edit" ng-trim="false" ng-model="todo.title" todo-escape="revertEdits(todo)" ng-blur="saveEdits(todo, 'blur')" todo-focus="todo == editedTodo">
    33                             </form>
    34                         </li>
    35                     </ul>
    36                 </section>
    37                 <footer id="footer" ng-show="todos.length" ng-cloak>
    38                     <span id="todo-count"><strong>{{remainingCount}}</strong>
    39                         <ng-pluralize count="remainingCount" when="{ one: 'item left', other: 'items left' }"></ng-pluralize>
    40                     </span>
    41                     <ul id="filters">
    42                         <li>
    43                             <a ng-class="{selected: status == ''} " href="#/">All</a>
    44                         </li>
    45                         <li>
    46                             <a ng-class="{selected: status == 'active'}" href="#/active">Active</a>
    47                         </li>
    48                         <li>
    49                             <a ng-class="{selected: status == 'completed'}" href="#/completed">Completed</a>
    50                         </li>
    51                     </ul>
    52                     <button id="clear-completed" ng-click="clearCompletedTodos()" ng-show="completedCount">Clear completed</button>
    53                 </footer>
    54             </section>
    55             <footer id="info">
    56                 <p>Double-click to edit a todo</p>
    57                 <p>Credits:
    58                     <a href="http://twitter.com/cburgdorf">Christoph Burgdorf</a>,
    59                     <a href="http://ericbidelman.com">Eric Bidelman</a>,
    60                     <a href="http://jacobmumm.com">Jacob Mumm</a> and
    61                     <a href="http://igorminar.com">Igor Minar</a>
    62                 </p>
    63                 <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
    64             </footer>
    65         </script>
    66 <script src="node_modules/angular/angular.js"></script>
    67 <script src="node_modules/angular-route/angular-route.js"></script>
    68 <script src="js/app.js"></script>
    69 <script src="js/controllers/todoCtrl.js"></script>
    70 <script src="js/services/todoStorage.js"></script>
    71 <script src="js/directives/todoFocus.js"></script>
    72 <script src="js/directives/todoEscape.js"></script>
    73     </body>
    74 </html>
    index.html
     1 var routeConfig = {
     2     controller: 'TodoCtrl',
     3     templateUrl: 'todomvc-index.html',
     4     resolve: {
     5       store: function (todoStorage) {
     6         // Get the correct module (API or localStorage).
     7         return todoStorage.then(function (module) {
     8           module.get(); // Fetch the todo records in the background.
     9           return module;
    10         });
    11       }
    12     }
    13 };
    14 
    15 $routeProvider
    16     .when('/', routeConfig)
    17     .when('/:status', routeConfig)
    18     .otherwise({
    19         redirectTo: '/'
    20     });

    这个代码现在基本看不懂,大概意思应该就是根据路由执行config中的逻辑,将模板展示在页面上,其中index.html有一段代码应该是用于替换模板的:

    <ng-view />

    我们先抛开那段看不懂的,直奔主流程,目光聚焦到控制器controller:

      1 angular.module('todomvc')
      2     .controller('TodoCtrl', function TodoCtrl($scope, $routeParams, $filter, store) {
      3         'use strict';
      4 
      5         var todos = $scope.todos = store.todos;
      6 
      7         $scope.newTodo = '';
      8         $scope.editedTodo = null;
      9 
     10         $scope.$watch('todos', function () {
     11             $scope.remainingCount = $filter('filter')(todos, { completed: false }).length;
     12             $scope.completedCount = todos.length - $scope.remainingCount;
     13             $scope.allChecked = !$scope.remainingCount;
     14         }, true);
     15 
     16         // Monitor the current route for changes and adjust the filter accordingly.
     17         $scope.$on('$routeChangeSuccess', function () {
     18             var status = $scope.status = $routeParams.status || '';
     19             $scope.statusFilter = (status === 'active') ?
     20                 { completed: false } : (status === 'completed') ?
     21                 { completed: true } : {};
     22         });
     23 
     24         $scope.addTodo = function () {
     25             var newTodo = {
     26                 title: $scope.newTodo.trim(),
     27                 completed: false
     28             };
     29 
     30             if (!newTodo.title) {
     31                 return;
     32             }
     33 
     34             $scope.saving = true;
     35             store.insert(newTodo)
     36                 .then(function success() {
     37                     $scope.newTodo = '';
     38                 })
     39                 .finally(function () {
     40                     $scope.saving = false;
     41                 });
     42         };
     43 
     44         $scope.editTodo = function (todo) {
     45             $scope.editedTodo = todo;
     46             // Clone the original todo to restore it on demand.
     47             $scope.originalTodo = angular.extend({}, todo);
     48         };
     49 
     50         $scope.saveEdits = function (todo, event) {
     51             // Blur events are automatically triggered after the form submit event.
     52             // This does some unfortunate logic handling to prevent saving twice.
     53             if (event === 'blur' && $scope.saveEvent === 'submit') {
     54                 $scope.saveEvent = null;
     55                 return;
     56             }
     57 
     58             $scope.saveEvent = event;
     59 
     60             if ($scope.reverted) {
     61                 // Todo edits were reverted-- don't save.
     62                 $scope.reverted = null;
     63                 return;
     64             }
     65 
     66             todo.title = todo.title.trim();
     67 
     68             if (todo.title === $scope.originalTodo.title) {
     69                 $scope.editedTodo = null;
     70                 return;
     71             }
     72 
     73             store[todo.title ? 'put' : 'delete'](todo)
     74                 .then(function success() {}, function error() {
     75                     todo.title = $scope.originalTodo.title;
     76                 })
     77                 .finally(function () {
     78                     $scope.editedTodo = null;
     79                 });
     80         };
     81 
     82         $scope.revertEdits = function (todo) {
     83             todos[todos.indexOf(todo)] = $scope.originalTodo;
     84             $scope.editedTodo = null;
     85             $scope.originalTodo = null;
     86             $scope.reverted = true;
     87         };
     88 
     89         $scope.removeTodo = function (todo) {
     90             store.delete(todo);
     91         };
     92 
     93         $scope.saveTodo = function (todo) {
     94             store.put(todo);
     95         };
     96 
     97         $scope.toggleCompleted = function (todo, completed) {
     98             if (angular.isDefined(completed)) {
     99                 todo.completed = completed;
    100             }
    101             store.put(todo, todos.indexOf(todo))
    102                 .then(function success() {}, function error() {
    103                     todo.completed = !todo.completed;
    104                 });
    105         };
    106 
    107         $scope.clearCompletedTodos = function () {
    108             store.clearCompleted();
    109         };
    110 
    111         $scope.markAll = function (completed) {
    112             todos.forEach(function (todo) {
    113                 if (todo.completed !== completed) {
    114                     $scope.toggleCompleted(todo, completed);
    115                 }
    116             });
    117         };
    118     });
    View Code

    这段代码130行不到,让我体会到了深深的神奇,首先我们在app中返回了读取到localstorage的对象:

    1 resolve: {
    2     store: function (todoStorage) {
    3       // Get the correct module (API or localStorage).
    4       return todoStorage.then(function (module) {
    5         module.get(); // Fetch the todo records in the background.
    6         return module;
    7       });
    8     }
    9 }

    然后就在controller的依赖项中读到了被注入的对象:

    var todos = $scope.todos = store.todos;

    此时,模板也被插到了页面上,等待controller的执行:

    首先这里有一个$watch方法,监控着todos的变化,每次变化都会体现到这里,导致view的变化:

    1 $scope.$watch('todos', function () {
    2     $scope.remainingCount = $filter('filter')(todos, { completed: false }).length;
    3     $scope.completedCount = todos.length - $scope.remainingCount;
    4     $scope.allChecked = !$scope.remainingCount;
    5 }, true);

    然后我们将关注点放在新增项目上:

     1 $scope.addTodo = function () {
     2     var newTodo = {
     3         title: $scope.newTodo.trim(),
     4         completed: false
     5     };
     6 
     7     if (!newTodo.title) {
     8         return;
     9     }
    10 
    11     $scope.saving = true;
    12     store.insert(newTodo)
    13         .then(function success() {
    14             $scope.newTodo = '';
    15         })
    16         .finally(function () {
    17             $scope.saving = false;
    18         });
    19 };

    View上的调用点是:

    1 <header id="header">
    2     <h1>todos</h1>
    3     <form id="todo-form" ng-submit="addTodo()">
    4         <input id="new-todo" placeholder="What needs to be done?" ng-model="newTodo" ng-disabled="saving" autofocus>
    5     </form>
    6 </header>

    首先这段代码中有一个autofocus的指令,没有什么卵用:

     1 angular.module('todomvc')
     2     .directive('todoFocus', function todoFocus($timeout) {
     3         'use strict';
     4 
     5         return function (scope, elem, attrs) {
     6             scope.$watch(attrs.todoFocus, function (newVal) {
     7                 if (newVal) {
     8                     $timeout(function () {
     9                         elem[0].focus();
    10                     }, 0, false);
    11                 }
    12             });
    13         };
    14     });
    View Code

    可以看到model直接绑定到了该文本框上,所以addTodo方法可以直接根据$scope获取文本框的属性,完了调用单例store提供的静态方法存储数据,saving参数可以暂时将文本框变成不可编辑状态,而后todo数据更新,会自动引发View变化,于是流程结束!!!

    我们如果将$scope放到全局上对其数据造成变化:

    window.sss = $scope;
    //控制台中造成变化
    sss.todos.pop()

    每次返回操作视图时候,该变化会马上反应到View上,于是我发现了以下不同:

    ① 因为所有与业务相关的数据全部做了双向绑定,我根本没有必要由dom获取数据了,我自然而然的到$scope中获取数据,不知道为什么,这个特性让我有点愉悦!

    ② 我要做的事情其实就是约定好数据对象,然后将该对象放到要用到的所有视图上即可,每次内存中数据变化Dom会同步更新

    于是通过以上两点,我似乎得到了一个惊人的结论:

    似乎我一旦配置好ng-model后,我要做的事情仅仅是操作$scope上的数据!!!

    因为,前端要做的事情只不过是正确的展示服务器端的数据,每次DOM事件造成的改变也往往是数据引起的,如果我们能做到数据变化自动更新到DOM变化的话,那么DOM操作的必要似乎没有了,而angular干的事情正是如此!!!

    思考

    到此为止,TodoMVC的代码我虽然没有完全看懂,但是他带给我的震撼是全方位的,之前使用MVC类框架可以规范数据到DOM的操作,很大程度上解除DOM和JavaScript的耦合关系,而angular似乎完全抛开了业务数据导致的DOM变化操作!!!

    我们现在团队有一mis后台系统,我在考虑是否要把它接过来,使用angular+bootstrap重构,可能别有一番风味吧!

    最后,今天初步调研了一下angularJS,就已经感受到他的魅力了,后面时间需要将之用于实践,并且对其设计思想作深入研究!!!

  • 相关阅读:
    Leetcode 16.25 LRU缓存 哈希表与双向链表的组合
    Leetcode437 路径总和 III 双递归与前缀和
    leetcode 0404 二叉树检查平衡性 DFS
    Leetcode 1219 黄金矿工 暴力回溯
    Leetcode1218 最长定差子序列 哈希表优化DP
    Leetcode 91 解码方法
    Leetcode 129 求根到叶子节点数字之和 DFS优化
    Leetcode 125 验证回文串 双指针
    Docker安装Mysql记录
    vmware虚拟机---Liunx配置静态IP
  • 原文地址:https://www.cnblogs.com/yexiaochai/p/4855780.html
Copyright © 2020-2023  润新知