01、AngularJS路由
本文是自己学习angularJS的第一篇总结,并不是一个step by step 的教程,而是将自己学习过程中有疑惑的地方记录下来。考虑到之后的AngularJS总结也将依赖这个应用,因此本文会在前面两个章节具体的介绍应用搭建过程,在之后的总结中,将不再涉及应用搭建的内容。
路由是前端构建单页面应用(SPA)必不可少的一部分。AngularJS中有两种路由实现,一个是内置的ngRouter,还有一个是基于 ngRoute 开发的第三方路由模块uiRouter。考虑到目前主要使用的是功能更强大uiRouter模块,因此本文将主要介绍uiRouter的使用。
需要注意的是,AngularJS1.2以后做了模块化的处理,也就是路由没有包含在Angular.js这个文件里面,而是把它独立出来成了一个模块。也就是说,不管是 使用ngRouter还是uiRouter,都需要单独引入模块文件。ngRouter对应的外部 js文件是 angular-route.js,uiRouter对应的外部文件是angular-ui-router.js。引入模块的方式有两种,一种是直接在index.html中中引入文件模块文件,这种方式比较简单便捷,甚至可以不用单独下载,直接引入cdn地址,如下:
<script src="http://cdn.static.runoob.com/libs/angular.js/1.4.6/angular.min.js"></script>
<script src="http://apps.bdimg.com/libs/angular-route/1.3.13/angular-route.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
这里的地址是分别从不同的地方找过来的,有出入可以在网上查查
还有一种方式是使用npm包管理器安装依赖。为了响应前端工程化开发,这里使用npm包管理器安装依赖,考虑到有些同学坑没有接触了模块化,详细的介绍以下npm配合webpack搭建前端应用的过程。
新建应用
创建一个项目并安装依赖。
- 利用npm 初始化一个项目。
mkdir routerStart
cd routerStart
npm init
- 安装依赖
查看angular 版本: npm view angular version
根据查出来的版本进行安装
安装angular :npm install --save angular@1.6.6
安装angular-route:npm install --save angular-route@1.6.6
安装@uirouter/angularjs:npm install --save @uirouter/angularjs@1.0.10
也可以直接在这个网站查看各个包的信息:npm包管理器
启动应用
以上已经安装好了最重要的三个依赖包,但为了能够更方便便捷的进行测试,再引入一个概念:webpack。可以把webpack看作是一个组织模块的框架,它的作用是:分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。有关于webpack的详细讲解配置,webpack
接下来新建几个文件:index.html、index.js、webpack.config.js。其中index.htm作为在浏览器上展示的界面,index.js是index.html中用到的脚本,webpack.config.js是webpack的配置文件,各个文件的内容如下:
//package.json配置文件
{
"name": "routerstart",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack"
},
"author": "spilledyear",
"license": "ISC",
"dependencies": {
"angular": "^1.6.6",
"angular-route": "^1.6.6",
"@uirouter/angularjs": "^1.0.10",
"webpack": "^3.8.1"
}
}
//webpack.config.js
module.exports = {
entry: './index.js',
output: {
filename: './bundle.js',
},
module: {
rules: [{
// test: /\.less$/,
// use: [{
// loader: "style-loader" // creates style nodes from JS strings
// }, {
// loader: "css-loader" // translates CSS into CommonJS
// }, {
// loader: "less-loader" // compiles Less to CSS
// }]
}]
}
};
//index.js
import angular from 'angular';
import ngRoute from 'angular-route';
import uiRouter from '@uirouter/angularjs';
var app = angular.module("app", [])
.controller("appCtrl",function($scope){
$scope.name = "hello!";
});
index.html
<html ng-app="app" ng-controller="appCtrl">
<head>
<title>test</title>
<meta charset="utf-8">
</head>
<body>
<input ng-model="name"/>
<h1>{{name}}</h1>
<script src="bundle.js"></script>
</body>
</html>
需要注意index.html中的这部分内容
<script src="bundle.js"></script>
并没有直接引用index.js文件,而是引入了bundle.js文件?这里会让人产生疑惑,为什不引入index.js文件?bundle.js文件从哪里来?这也是前端模块化工程与非模块化工程的主要区别,在非模块化工程中,习惯性的将所有依赖的js框架引入到html界面,这种做法造成的后果就是项目结构组织混乱、文件依赖关系错综复杂,代码功底不够还会让各种全局满天飞。模块化很好的解决了这些问题,想知道模块化更具体的细节,请自行google。而这里引入的bundle.js文件,其实和webpack有关,如果此时没有利用webpack做任何事情,仅仅是用浏览器打开index.html文件,那么肯定是可以在控制台看到报错信息的,此时的bundle.js文件是不存在的。在webpack的配置文件中,有这么一段内容:
entry: './index.js',
output: {
filename: './bundle.js',
},
根据字面的意思,应该可以猜到这段代码的用意,事实也是如此,entry和output是webpack中的两个概念,entry作为整个应用的入口文件,webpack会找到入口文件,然后根据这个文件里面的内容依次加载其它依赖的模块,然后进行编译等操作,最后将所有编译后结果输出到output 配置的输出文件中,文中配置的是bundle.js,这就是为什么只需要在index.html引入bundle.js的原因。如果你有仔细观察bundle.js文件的话,会发现这个文件很大(最少有1M),这就很好的说明了它是编译后的产物。
还有一个需要注意的地方是package.json文件
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack"
},
在scripts脚本中配置了start命令,大概就是说在执行npm start的时候相当于执行了 webpack命令。具体的细节还请参照这篇文章:webpack
接下来是启动过程
在命令行以下执行
npm start
如果一切正常,会在命令行下得到以下信息
image.png
这时候再观察应用目录结构,会发现根目录下多了一个bundle.js文件,此时完全可以把 npm start 当作是一个编译过程。
准备工作完毕,直接在浏览器下打开index.html文件,运行结果正常
image.png
如果模块化工程开发过程是这样的,那也太麻烦了吧。可能这样吗?不存在的。想使用更强大的功能,只需要修改webpack配置文件就可以了,比如开发中最期待的热部署。考虑到之后的许多AngularJS特性都需要利用这个应用来测试,在此贴出webpack比较全面的配置文件(以后可能还会添加),想了解详情请跳转上文中提到的链接
//webpack.congfigjs
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './app/index.js',
output: {
path: __dirname + "/dist",
filename: "app.js"
},
devtool: 'eval-source-map',
devServer: {
port: 9000, //端口
contentBase: "./app", //本地服务器所加载的页面所在的目录
historyApiFallback: true, //不跳转
inline: true //实时刷新
},
module: {
rules: [
{
test: /(\.jsx|\.js)$/,
use: {
loader: "babel-loader"
},
exclude: /node_modules/
},
{
test: /\.less$/,
use: [{
loader: "style-loader" // creates style nodes from JS strings
}, {
loader: "css-loader", // translates CSS into CommonJS
options: {
modules: true
}
}, {
loader: "less-loader" // compiles Less to CSS
}]
}]
},
plugins: [
//new 一个这个插件的实例,并传入相关的参数
new webpack.BannerPlugin('版权所有,翻版必究'),
new HtmlWebpackPlugin({
template: __dirname + "/app/index.tmpl.html"
})
],
};
同时,应用的目录结构也做了相应的调整,如下:
image.png
app目录下存放的是源文件,dist下是编译后的输出文件。其它几个配置文件的配置信息如下:
//.babelrc
{
"presets": ["es2015", "stage-0"] //babelr支持的模块
}
//package.json
{
"name": "routerstart",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --open",
"webpack": "webpack"
},
"author": "spilledyear",
"license": "ISC",
"dependencies": {
"angular": "^1.6.6",
"angular-route": "^1.6.6",
"@uirouter/angularjs": "^1.0.10",
"bootstrap": "^3.3.7"
},
},
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"html-webpack-plugin": "^2.30.1",
"webpack": "^3.8.1",
"webpack-dev-server": "^2.9.3"
}
}
工作完毕,命令行下直接执行以下命令
npm start
浏览器自动打开,结果和刚才的一样,同时,修改源代码,浏览器也会即时刷新。
注意,虽然应用运行正常,但是这里还有些问题。从package.json可以看出目前配置了两个命令,一个是 npm start,另一个是 npm run webpack。其中npm run webpack 完全是用来打包的, npm start 配置的是webpacke-dev,是为了方便调试。但是angularJS开发和react开发是有不同的,这里也没有用JSX语法,这就意味着需要将angularJS中的一些html文件搬到编译后的目录(dist),可能需要用gulp,为了不再增加应用的复杂性,这里不再引入gulp相关内容。因此,再之后的开发过程中,不需要考虑 npm run webpack 这个命令。如果需要打包,可能需要将app目录下的一些文件手动拷贝到dist目录下。
uiRouter
uiRouter是基于 ngRoute 开发的第三方路由模块,功能比ngRoute 强大,使用方式也比较简答,因此先介绍uiRouter。当然,最好的文档还是官方文档:ui-router ,例如:
image.png
uiRouter的首页就详细的介绍了与ngRoute的关系以及使用方式。通过api文档API可以发现,uiRouter 中存在这么几个概念 。
image.png
接下来介绍使用最多的几个。
模块
ui.router
服务
$urlRouterProvider
$stateProvider
指令
ui-sref
ui-view
首先需要明白“state” 是路由中非常重要的一个概念,在使用过程中,可以为每个state分配所对应的模块(界面),当需要改变界面时,只需要找到对应界面(模板)的那个state,最后uiRouter将模板加载到模板容器(ui-view),用户就可以在界面看到想要的效果。
$urlRouterProvider起着监视$location服务的作用。用官方的一段原话:
$urlRouterProvider has the responsibility of watching $location. When $location changes it runs through a list of rules one by one until a match is found. $urlRouterProvider is used behind the scenes anytime you specify a url in a state configuration. All urls are compiled into a UrlMatcher object (see $urlMatcherFactory below).
大概就是说当 $location发生改变的时候,$urlRouterProvider将会采用遍历的方式找到发生变化的那个选项。因为在配置state的时候是会绑定url的,而这个过程其实使用到了$urlRouterProvider。
$stateProvider用于管理state和控制state状态直接的转换,进而通过改变state改变界面效果。它提供了一些触发事件和回调函数,异步解析目标状态的任何依赖项,并更新$ location以反映当前状态。
ui-sref指令
Equivalent to href or ng-href in <a /> elements except the target value is a state name. Adds an appropriate href to the element according to the state associated URL.
可以作用在<a>标签上,它指是一个 state 对应的值。当点击链接的时候,就会找到state对应的URL,进而导航到对应的URl。
ui-view指令
Renders views defined in the current state. Essentially ui-view directives are (optionally named) placeholders that gets filled with views defined in the current state.
可以看作时一个界面的容器。通过路由加载的界面,都必须放到一个或者多哥ui-view容器中。它最主要的作用是呈现当前状态下定义的视图,也就意味着如果没有定义ui-view,当前state对应的视图就没办法展现,在界面上就看不到任何效果。
接下来通过一个实例来讲解uiRouter的使用方式。
//index.js
import angular from 'angular';
import ngRoute from 'angular-route';
import uiRouter from '@uirouter/angularjs';
var app = angular.module("app", [uiRouter])
.config(["$stateProvider", "$urlRouterProvider",
function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state("app", {
url: "/",
templateUrl: "index.html"
})
.state("page01", {
url: "/page01",
templateUrl: "template/page01.html"
})
.state("page02", {
url: "/page02",
templateUrl: "template/page02.html"
})
.state("page03", {
url: "/page03",
templateUrl: "template/page03.html"
})
}
])
.controller("appCtrl",function($scope, $state){
$scope.name = "hello!";
$scope.goPage = function(){
$state.go('page03');
}
});
//index.html
<html ng-app="app" ng-controller="appCtrl">
<head>
<title>test</title>
<meta charset="utf-8">
</head>
<body>
<div style="display: flex;flex-direction: row;justify-content: space-between;">
<div style="">
<div>
<a ui-sref="page01">page01</a>
</div>
<div>
<a ui-sref="page02">page02</a>
</div>
<div>
<button ng-click="goPage()">page03</button>
</div>
</div>
<div class="item">
<ui-view></ui-view>
</div>
</div>
<script type="text/javascript" src="index.js"></script>
</body>
</body>
</html>
从上可以看出,在index.js中定义了4个state, $stateProvider的state方法接收两个参数,第一个参数是 “state”的名称,第二个参数是“state"相关的配置,其中包括url和对应的视图模板(界面)。
image.png
同时,在index.html中,使用了ui-serf指令。如上所述,这个指令的作用其实和$state.go('xxx')是一样的,都是为了跳转到对应state的界面。通过改变state的指向从而改变ui-view 模板容器渲染的内容。
为了看到界面效果,还需要在app目录下添加一个template目录,这个目录的作用就是用来存放视图模板(界面、html文件)。因此,最终的目录结构如下:
image.png
执行 npm start 命令,在浏览器查看效果;
初始界面效果图如下
image.png
点击相应的按钮,观察浏览器地址栏的变化:
image1.png
image2.png
image3.png
可以很容易的发现,地址栏在变,界面右侧区域的内容也在变化,这就是一个最基本的路由系统。在此之余,简单的分析其中的一些细节和路由过程。
image.png
从上图可以看出,此处有两种路由实现,ui-sref 和 添加一个单击事件,然后在单击事件中完成逻辑处理。
简单看看 goPage() 实现:
$scope.goPage = function(){
$state.go('page03');
}
调用的就是 $state.go('x'x'x') 方法。
路由过程
点击对应链接,触发 $state.go('x'x'x') 方法,uiRouter根据state找到对应的视图并加载在ui-view容器中,实现界面的变化。一定要注意,是将视图模板加载到 ui-view容器中,没有ui-view容器,是不能够成功显示对应的模板内容的,这点必须要记住。
父子路由
父子路由实现的方式也比较简单。对index.js 稍作修改
import angular from 'angular';
import ngRoute from 'angular-route';
import uiRouter from '@uirouter/angularjs';
var app = angular.module("app", [uiRouter])
.config(["$stateProvider", "$urlRouterProvider",
function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state("app", {
url: "/",
templateUrl: "index.html"
})
.state("page01", {
url: "/page01",
templateUrl: "template/page01.html"
})
.state("page02", {
url: "/page02",
templateUrl: "template/page02.html"
})
.state("page03", {
url: "/page03",
templateUrl: "template/page03.html"
})
.state("page03.children", {
url: "/children",
views: {
'children': {
templateUrl: "template/children.html"
}
}
})
}
])
.controller("appCtrl",function($scope, $state){
$scope.name = "hello!";
$scope.goPage = function(){
$state.go('page03');
}
});
注意观察这一部分内容:
.state("page03.children", {
url: "/children",
views: {
'children': {
templateUrl: "template/children.html"
}
}
})
这是实现父子路由的关键部分,”page03.children“ ,以”."作为分隔符,代表“page03"作为一个父界面,
新的界面作为”page03"对应的一个子界面。同时,需要对page03.html文件做修改,page03.html文件内容如下:
//page03.html
<html>
<head>
<title>page03</title>
<meta charset="utf-8">
</head>
<body>
<div>
<h1>hello, page03</h1>
<a ui-sref="page03.children">children</a>
<div>
chirdren content area
<ui-view name="children"></ui-view>
</div>
</div>
</body>
</html>
image.png
同时,在template目录下添加一个新html文件: children.html,作为 page03的子界面。
// children.html
<html>
<head>
<title>children</title>
<meta charset="utf-8">
</head>
<body>
<div>
<h1>hello, children</h1>
</div>
</body>
</html>
查看界面效果:
page03.png
chirldre.png
观察浏览器地址栏变化,观察界面变化。