├── .gitignore ├── src ├── example │ ├── article │ │ ├── swiper1.png │ │ ├── swiper2.png │ │ ├── swiper3.png │ │ ├── swiper4.png │ │ ├── article.html │ │ ├── article.less │ │ └── article.js │ ├── app.js │ ├── index.html │ ├── lib │ │ ├── dataManager.js │ │ └── data.js │ ├── list │ │ ├── list.html │ │ └── list.js │ └── app.less ├── util.js └── index.js ├── dist ├── example │ ├── 47e04b2447b9170b0657988bf64d2126.png │ ├── 8d009c9d91ea14721eaff19d32237d40.png │ ├── b8506bdb1a762806f909f5c12cf8d92b.png │ ├── b87cd3b87e12ff32ce7c52b6c3a13945.png │ └── index.html └── router.min.js ├── .babelrc ├── webpack.prod.config.js ├── webpack.dev.config.js ├── package.json ├── README.md └── test └── unit └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | npm-debug.log 4 | publish.sh 5 | *.DS_Store -------------------------------------------------------------------------------- /src/example/article/swiper1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/src/example/article/swiper1.png -------------------------------------------------------------------------------- /src/example/article/swiper2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/src/example/article/swiper2.png -------------------------------------------------------------------------------- /src/example/article/swiper3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/src/example/article/swiper3.png -------------------------------------------------------------------------------- /src/example/article/swiper4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/src/example/article/swiper4.png -------------------------------------------------------------------------------- /dist/example/47e04b2447b9170b0657988bf64d2126.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/dist/example/47e04b2447b9170b0657988bf64d2126.png -------------------------------------------------------------------------------- /dist/example/8d009c9d91ea14721eaff19d32237d40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/dist/example/8d009c9d91ea14721eaff19d32237d40.png -------------------------------------------------------------------------------- /dist/example/b8506bdb1a762806f909f5c12cf8d92b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/dist/example/b8506bdb1a762806f909f5c12cf8d92b.png -------------------------------------------------------------------------------- /dist/example/b87cd3b87e12ff32ce7c52b6c3a13945.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/progrape/router/HEAD/dist/example/b87cd3b87e12ff32ce7c52b6c3a13945.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-3" 5 | ], 6 | "plugins": ["transform-object-assign", "transform-class-properties", "add-module-exports"] 7 | } -------------------------------------------------------------------------------- /src/example/app.js: -------------------------------------------------------------------------------- 1 | import Router from '../index'; 2 | import './app.less'; 3 | import list from './list/list'; 4 | import article from './article/article'; 5 | 6 | const router = new Router({ 7 | container: '#container', 8 | enterTimeout: 300, 9 | leaveTimeout: 300 10 | }); 11 | 12 | router.push(list) 13 | .push(article) 14 | .setDefault('/') 15 | .init(); -------------------------------------------------------------------------------- /src/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | example 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /src/example/lib/dataManager.js: -------------------------------------------------------------------------------- 1 | 2 | let data = {}; 3 | 4 | export default { 5 | /** 6 | * get data 7 | * @param {String} key 8 | * @returns {*} 9 | */ 10 | getData: function (key){ 11 | return data[key]; 12 | }, 13 | /** 14 | * set data 15 | * @param {String} key 16 | * @param {*} value 17 | */ 18 | setData: function (key, value){ 19 | data[key] = value; 20 | } 21 | }; -------------------------------------------------------------------------------- /dist/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | example 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /src/example/article/article.html: -------------------------------------------------------------------------------- 1 |
2 | {{each items as item i}} 3 |
4 | 5 |
6 | {{/each}} 7 |
8 |
9 |
10 |

{{article.title}}

11 |
12 |

{{article.summary}}

13 |
14 |
15 |

{{article.summary}}

16 |
17 |
18 |
-------------------------------------------------------------------------------- /src/example/article/article.less: -------------------------------------------------------------------------------- 1 | .swiper { 2 | height: 200px; 3 | overflow: hidden; 4 | transition: all 0.3s ease; 5 | } 6 | .item { 7 | height: 100%; 8 | background-position: center center; 9 | background-size: cover; 10 | position: relative; 11 | overflow: hidden; 12 | float: left; 13 | 14 | &.active{ 15 | .animated{ 16 | animation-fill-mode: both; 17 | opacity: 1; 18 | } 19 | } 20 | &:not(.active){ 21 | .animated{ 22 | animation: none; 23 | opacity: 0; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/example/list/list.html: -------------------------------------------------------------------------------- 1 |
2 |
图文组合列表
3 |
4 | {{each list as item i}} 5 | 6 |
7 | 8 |
9 |
10 |

{{item.title}}

11 |

{{item.summary}}

12 |
13 |
14 | {{/each}} 15 |
16 |
-------------------------------------------------------------------------------- /src/example/list/list.js: -------------------------------------------------------------------------------- 1 | import 'weui.js'; 2 | import template from 'art-template/dist/template-debug'; 3 | import remote from '../lib/data'; 4 | import dataManager from '../lib/dataManager'; 5 | import tpl from 'raw!./list.html'; 6 | 7 | export default { 8 | url: '/', 9 | render: function (callback) { 10 | 11 | const data = dataManager.getData('data'); 12 | if (data) { 13 | const html = template.compile(tpl)({list: data}); 14 | 15 | callback(null, html); 16 | } 17 | else { 18 | // 模拟异步加载数据 19 | $.weui.loading('加载中...'); 20 | setTimeout(() => { 21 | dataManager.setData('data', remote); 22 | const html = template.compile(tpl)({list: remote}); 23 | callback(null, html); 24 | $.weui.hideLoading(); 25 | }, 800); 26 | } 27 | }, 28 | bind: function () { 29 | 30 | } 31 | }; -------------------------------------------------------------------------------- /webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var pkg = require('./package.json'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | output: { 7 | path: './dist', 8 | filename: 'router.min.js', 9 | library: 'Router', 10 | libraryTarget: 'umd', 11 | umdNamedDefine: true 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | loader: 'babel' 19 | } 20 | ] 21 | }, 22 | plugins: [ 23 | new webpack.optimize.UglifyJsPlugin({ 24 | compress: { 25 | warnings: false 26 | } 27 | }), 28 | new webpack.BannerPlugin([ 29 | pkg.name + ' v' + pkg.version + ' (' + pkg.homepage + ')', 30 | 'Copyright ' + new Date().getFullYear(), 31 | 'Licensed under the '+ pkg.license +' license' 32 | ].join('\n')) 33 | ] 34 | }; -------------------------------------------------------------------------------- /src/example/app.less: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html, body{ 7 | height: 100%; 8 | } 9 | 10 | body { 11 | overflow-x: hidden; 12 | } 13 | 14 | .container { 15 | height: 100%; 16 | overflow-y: auto; 17 | -webkit-overflow-scrolling: touch; 18 | & > div { 19 | background-color: #fff; 20 | } 21 | } 22 | 23 | @keyframes slideIn { 24 | from { 25 | transform: translate3d(100%, 0, 0); 26 | opacity: 0; 27 | } 28 | to { 29 | transform: translate3d(0, 0, 0); 30 | opacity: 1; 31 | } 32 | } 33 | 34 | @keyframes slideOut { 35 | from { 36 | transform: translate3d(0, 0, 0); 37 | opacity: 1; 38 | } 39 | to { 40 | transform: translate3d(100%, 0, 0); 41 | opacity: 0; 42 | } 43 | } 44 | 45 | .enter, .leave { 46 | position: absolute; 47 | top: 0; 48 | right: 0; 49 | bottom: 0; 50 | left: 0; 51 | z-index: 1; 52 | } 53 | 54 | .enter { 55 | animation: slideIn .2s forwards; 56 | } 57 | 58 | .leave { 59 | animation: slideOut .25s forwards; 60 | } -------------------------------------------------------------------------------- /src/example/article/article.js: -------------------------------------------------------------------------------- 1 | import template from 'art-template/dist/template-debug'; 2 | import Swiper from 'iswiper'; 3 | import dataManager from '../lib/dataManager'; 4 | import tpl from 'raw!./article.html'; 5 | import './article.less'; 6 | 7 | // 图片来自微软 cn.bing.com , 版权归原作者所有 8 | import swiper1 from './swiper1.png'; 9 | import swiper2 from './swiper2.png'; 10 | import swiper3 from './swiper3.png'; 11 | import swiper4 from './swiper4.png'; 12 | 13 | export default { 14 | url: '/article/:id', 15 | render: function () { 16 | const id = this.params.id; 17 | const data = dataManager.getData('data'); 18 | const article = data.filter(article => article.id == id)[0]; 19 | const html = template.compile(tpl)({article: article, items: [swiper1, swiper2, swiper3, swiper4]}); 20 | 21 | // 可以返回一个 promise 22 | return new Promise((resolve, reject) => { 23 | resolve(html); 24 | }); 25 | // 也可以直接返回 html 26 | // return html; 27 | }, 28 | bind: function () { 29 | const swiper = new Swiper({ 30 | direction: 'horizontal' 31 | }); 32 | swiper.on('swiped', (prev, current) => { 33 | console.log('prev', prev); 34 | console.log('current', current); 35 | }); 36 | } 37 | }; -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | import pathToRegexp from 'path-to-regexp'; 2 | 3 | /** 4 | * get hash by full url 5 | * @param {String} url 6 | * @returns {string} 7 | */ 8 | export function getHash(url) { 9 | return url.indexOf('#') !== -1 ? url.substring(url.indexOf('#') + 1) : '/'; 10 | } 11 | 12 | /** 13 | * get route from routes filter by url 14 | * @param {Array} routes 15 | * @param {String} url 16 | * @returns {Object} 17 | */ 18 | export function getRoute(routes, url) { 19 | for (let i = 0, len = routes.length; i < len; i++) { 20 | let route = routes[i]; 21 | let keys = []; 22 | const regex = pathToRegexp(route.url, keys); 23 | const match = regex.exec(url); 24 | if (match) { 25 | route.params = {}; 26 | for (let j = 0, l = keys.length; j < l; j++) { 27 | const key = keys[j]; 28 | const name = key.name; 29 | route.params[name] = match[j + 1]; 30 | } 31 | return route; 32 | } 33 | } 34 | return null; 35 | } 36 | 37 | /** 38 | * has children 39 | * @param {HTMLElement} parent 40 | * @returns {boolean} 41 | */ 42 | export function hasChildren(parent) { 43 | const children = parent.children; 44 | return children.length > 0; 45 | } 46 | 47 | /** 48 | * noop 49 | */ 50 | export function noop() { 51 | 52 | } -------------------------------------------------------------------------------- /src/example/lib/data.js: -------------------------------------------------------------------------------- 1 | 2 | export default [{ 3 | id: 1, 4 | title: '微信Web App开发最佳实践', 5 | cover: 'https://mmrb.github.io/avatar/kl.jpg', 6 | summary: '组内小伙伴 jf 去 Feday 广州站的分享,有蛮多干货,都是微信 web app 开发者们关注的一些问题。 不做过多介绍,没有去现场听得朋友可以下载ppt来看。 感兴趣的朋友也可以下载广州站的所有ppt,以及关注接下来的 Feday 日程。' 7 | }, { 8 | id: 2, 9 | title: 'X5即将升级内核到Blink', 10 | cover: 'https://mmrb.github.io/avatar/shenfei.jpg', 11 | summary: '我们从QQ浏览器团队得到消息,X5已经完成升级到Blink的开发工作,最近已经开始下发到客户端中,而X5内核的更新是热更新,也就是说不需要用户更新微信客户端,在良好的网络环境下(比如WiFi)会在后台静默更新。 根据我们拿到的版本,X5用的Blink版本是Chrome 37。虽然' 12 | }, { 13 | id: 3, 14 | title: 'WeUI的设计稿开源', 15 | cover: 'https://mmrb.github.io/avatar/bear.jpg', 16 | summary: '自从 WeUI 开源后,已经收到 7000 多 star ,将近 2000 的 fork,我们在欣喜之余,也收到了蛮多有价值的意见与建议,其中之一就是将设计稿开源——好吧,其实设计稿叫『开放下载』更合适一些。 那我们今天开放了基于 WeUI 0.4 版本的设计稿 sketch 文' 17 | }, { 18 | id: 4, 19 | title: 'HTML5+CSS3 loading 效果收集', 20 | cover: 'https://mmrb.github.io/avatar/gaby.jpg', 21 | summary: '用gif图片来做loading的时代已经过去了,它显得太low了,而用HTML5/CSS3以及SVG和canvas来做加载动画显得既炫酷又逼格十足。这已经成为一种趋势。 这里收集了几十个用html5和css3实现的loading效果,以供学习参考。 01. CSS Rainbow' 22 | }, { 23 | id: 5, 24 | title: '微信网页开发者工具发布', 25 | cover: 'https://mmrb.github.io/avatar/xx.jpg', 26 | summary: '兄弟团队内测已久的微信网页开发者工具终于在今天的微信公开课Pro大会上发布了,喜大普奔。 这个工具有主要有3个功能: 使用真实用户身份,调试微信网页授权。 校验页面的 JSSDK 权限,以及模拟大部分 SDK 的输入和输出。 利用集成的 Chrome DevTools 和基本' 27 | }]; -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var autoprefixer = require('autoprefixer'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var OpenBrowserPlugin = require('open-browser-webpack-plugin'); 6 | 7 | module.exports = { 8 | context: path.join(__dirname, 'src/example'), 9 | entry: { 10 | js: './app.js' 11 | }, 12 | output: { 13 | path: path.join(__dirname, 'dist/example'), 14 | filename: 'example.js' 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.jsx?$/, 20 | exclude: /node_modules/, 21 | loader: 'babel' 22 | }, { 23 | test: /\.less$/, 24 | exclude: /node_modules/, 25 | loader: 'style!css!postcss!less' 26 | }, { 27 | test: /\.png/, 28 | exclude: /node_modules/, 29 | loader: 'file' 30 | } 31 | ] 32 | }, 33 | postcss: [autoprefixer], 34 | resolve: { 35 | extensions: ['', '.js', '.jsx'] 36 | }, 37 | plugins: [ 38 | new webpack.ProvidePlugin({ 39 | $: 'jquery', 40 | jQuery: 'jquery' 41 | }), 42 | new HtmlWebpackPlugin({ 43 | template: path.join(__dirname, 'src/example/index.html') 44 | }), 45 | new OpenBrowserPlugin({url: 'http://localhost:8080'}) 46 | ], 47 | devServer: { 48 | contentBase: './dist', 49 | hot: true 50 | } 51 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "router", 3 | "version": "0.1.0", 4 | "description": "router", 5 | "main": "dist/router.min.js", 6 | "scripts": { 7 | "start": "webpack-dev-server --config webpack.dev.config.js --hot --inline --progress --colors --port 8080 --host 0.0.0.0", 8 | "build": "npm run build:lib && npm run build:example", 9 | "build:lib": "webpack --config webpack.prod.config.js --progress --colors", 10 | "build:example": "webpack --config webpack.dev.config.js --progress --colors", 11 | "test": "mocha --compilers js:babel-core/register --recursive", 12 | "clean": "rimraf ./dist" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/progrape/router.git" 17 | }, 18 | "keywords": [ 19 | "router", 20 | "weui", 21 | "demo" 22 | ], 23 | "author": "jf ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/progrape/router/issues" 27 | }, 28 | "homepage": "https://github.com/progrape/router", 29 | "devDependencies": { 30 | "art-template": "^3.0.3", 31 | "autoprefixer": "^6.3.3", 32 | "babel-core": "^6.9.0", 33 | "babel-loader": "^6.2.4", 34 | "babel-plugin-add-module-exports": "^0.1.2", 35 | "babel-plugin-transform-class-properties": "^6.6.0", 36 | "babel-plugin-transform-object-assign": "^6.8.0", 37 | "babel-preset-es2015": "^6.6.0", 38 | "babel-preset-stage-3": "^6.5.0", 39 | "chai": "^3.5.0", 40 | "css-loader": "^0.23.1", 41 | "file-loader": "^0.8.5", 42 | "html-webpack-plugin": "^2.9.0", 43 | "iswiper": "^1.4.1", 44 | "jquery": "^2.2.3", 45 | "jsdom": "^9.1.0", 46 | "less": "^2.6.1", 47 | "less-loader": "^2.2.2", 48 | "mocha": "^2.4.5", 49 | "open-browser-webpack-plugin": "0.0.2", 50 | "postcss-loader": "^0.8.1", 51 | "raw-loader": "^0.5.1", 52 | "rimraf": "^2.5.2", 53 | "style-loader": "^0.13.0", 54 | "url-loader": "^0.5.7", 55 | "webpack": "^1.12.14", 56 | "webpack-dev-server": "^1.14.1", 57 | "weui.js": "^0.2.1" 58 | }, 59 | "dependencies": { 60 | "path-to-regexp": "^1.3.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Simple Router 2 | 3 | A very simple router, just for the **demo** of [weui](https://github.com/weui/weui) 4 | 5 | ### 预览 6 | 7 | ![image](https://cloud.githubusercontent.com/assets/4652816/14374852/6cc83df2-fd8d-11e5-8721-097136667f89.png) 8 | 9 | https://progrape.github.io/router 10 | 11 | ### 使用 12 | 13 | HTML 14 | 15 | ```html 16 | 17 | 18 | 19 | 20 | 21 | example 22 | 23 | 24 |
25 | 26 | 27 | 28 | ``` 29 | 30 | JavaScript 31 | 32 | ```javascript 33 | var router = new Router({ 34 | container: '#container' 35 | }); 36 | var home = { 37 | url: '/', 38 | className: 'home', 39 | render: function (){ 40 | return '

home

'; 41 | } 42 | }; 43 | var post = { 44 | url: '/post/:id', 45 | className: 'post', 46 | render: function (){ 47 | var id = this.params.id; 48 | return '

post

'; 49 | } 50 | }; 51 | router.push(home).push(post).setDefault('/').init(); 52 | ``` 53 | 54 | ### 运行示例 55 | 56 | ```shell 57 | git clone https://github.com/progrape/router 58 | cd router 59 | npm install 60 | npm start 61 | ``` 62 | 63 | ### API 64 | 65 | #### Router([option]) 66 | 67 | 参数 `option` 是可选的,下面是该参数可选的属性。 68 | 69 | |属性 |类型 |默认值 |描述| 70 | |-------- |--- |--- |--- 71 | |container |String |'#container' | `container` 容器的选择器 72 | |enter |String |'enter' | 该页面出现时添加的类名,`enterTimeout` 为 0 时会被忽略 73 | |enterTimeout |Number |0 | 在这个时间之后移除添加的 `enter` 类名 74 | |leave |String |'leave' | 该页面离开时添加的类名,`lieaveTimeout` 为 0 时会被忽略 75 | |leaveTimeout |Number |0 | 在这个时间之后移除该页面的 DOM 76 | 77 | 78 | #### 实例方法 79 | 80 | 以下方法执行完毕后均返回实例本身。 81 | 82 | ##### push(route) 83 | 84 | 添加路由页面的配置。下面是 `route` 参数的属性。 85 | 86 | 87 | |属性 |类型 |描述 88 | |-----------|-------|--- 89 | |url |String | 以 `/` 开头的 url,会体现在 hash,支持参数,如:`/user/:userId/post/:posdId` 90 | |className |String | 可选,该页面可以添加的额外类名,以便控制该页面下的样式 91 | |render |function| 页面渲染方法,支持同步和异步, 可以直接返回 html 字符串,可以返回 `promise` 对象,也可以接收 `callback` 参数 92 | |bind |function| 执行绑定事件的方法,`this` 指向当前页面容器 93 | 94 | route 示例如下: 95 | 96 | 同步 97 | 98 | ```javascript 99 | { 100 | url: '/home', 101 | className: 'home', 102 | render: function (){ 103 | return ''; 104 | }, 105 | bind: function (){ 106 | $(this).on('click', 'button', function (){ 107 | // do something 108 | }); 109 | } 110 | } 111 | ``` 112 | 113 | promise 114 | 115 | ```javascript 116 | { 117 | url: '/home', 118 | className: 'home', 119 | render: function (){ 120 | return new Promise(function (resolve, reject){ 121 | resolve(''); 122 | }); 123 | }, 124 | bind: function (){ 125 | $(this).on('click', 'button', function (){ 126 | // do something 127 | }); 128 | } 129 | } 130 | ``` 131 | 132 | callback 133 | 134 | ```javascript 135 | { 136 | url: '/home', 137 | className: 'home', 138 | render: function (callback){ 139 | callback(null, ''); 140 | }, 141 | bind: function (){ 142 | $(this).on('click', 'button', function (){ 143 | // do something 144 | }); 145 | } 146 | } 147 | ``` 148 | 149 | 150 | ##### setDefault(url) 151 | 152 | 设置页面启动时默认跳转的 url。 153 | 154 | ##### init() 155 | 156 | 启动页面,在调用完 `push` 和 `setDefault` 方法后调用,主要完成 `hashchange` 的事件监听和跳转默认页面的工作。 157 | 158 | ### License 159 | 160 | The MIT License(http://opensource.org/licenses/MIT) 161 | 162 | 请自由地享受和参与开源 163 | -------------------------------------------------------------------------------- /test/unit/util.js: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai'; 2 | import {jsdom} from 'jsdom'; 3 | import * as util from '../../src/util'; 4 | 5 | describe('util.getHash', () => { 6 | it('should return "/" when no hash', () => { 7 | const url = 'https://weui.com'; 8 | const hash = util.getHash(url); 9 | expect(hash).to.be.equal('/'); 10 | }); 11 | 12 | it('should return "" when empty hash', () => { 13 | const url = 'https://weui.com/#'; 14 | const hash = util.getHash(url); 15 | expect(hash).to.be.equal(''); 16 | }); 17 | 18 | it('should return "/home" with hash', () => { 19 | const url = 'https://weui.com/#/home'; 20 | const hash = util.getHash(url); 21 | expect(hash).to.be.equal('/home'); 22 | }); 23 | }); 24 | 25 | describe('util.getRoute', () => { 26 | const routes = [{ 27 | url: '/home', 28 | render: function () { 29 | return `home`; 30 | } 31 | }, { 32 | url: '/post/:id?', 33 | render: function () { 34 | const id = this.params.id; 35 | return `post ${id}`; 36 | } 37 | }, { 38 | url: '/user/:userId/post/:postId', 39 | render: function () { 40 | const userId = this.params['userId']; 41 | return `user ${userId}`; 42 | } 43 | }]; 44 | 45 | it('should return null', () => { 46 | const route = util.getRoute(routes, '/category'); 47 | expect(route).to.be.equal(null); 48 | }); 49 | 50 | it('should return route', () => { 51 | const route = util.getRoute(routes, '/home'); 52 | expect(route).to.be.not.equal(null); 53 | expect(route.url).to.be.equal(routes[0].url); 54 | expect(route.render).to.be.equal(routes[0].render); 55 | }); 56 | 57 | it('should return route', () => { 58 | const route = util.getRoute(routes, `/post`); 59 | expect(route).to.be.not.equal(null); 60 | expect(route.url).to.be.equal(routes[1].url); 61 | expect(route.render).to.be.equal(routes[1].render); 62 | expect(route.params.id).to.be.equal(undefined); 63 | }); 64 | 65 | it('should return route', () => { 66 | const id = '2'; 67 | const route = util.getRoute(routes, `/post/${id}`); 68 | expect(route).to.be.not.equal(null); 69 | expect(route.url).to.be.equal(routes[1].url); 70 | expect(route.render).to.be.equal(routes[1].render); 71 | expect(route.params.id).to.be.equal(id); 72 | }); 73 | 74 | it('should return route', () => { 75 | const userId = '2'; 76 | const postId = '3'; 77 | const route = util.getRoute(routes, `/user/${userId}/post/${postId}`); 78 | expect(route).to.be.not.equal(null); 79 | expect(route.url).to.be.equal(routes[2].url); 80 | expect(route.render).to.be.equal(routes[2].render); 81 | expect(route.params['userId']).to.be.equal(userId); 82 | expect(route.params['postId']).to.be.equal(postId); 83 | }); 84 | }); 85 | 86 | describe('util.hasChildren', () => { 87 | it('should have no children when body is empty', () => { 88 | const document = jsdom(' '); 89 | const body = document.body; 90 | 91 | const hasChildren = util.hasChildren(body); 92 | 93 | expect(hasChildren).to.be.equal(false); 94 | }); 95 | 96 | it('should have no children when body is empty', () => { 97 | const document = jsdom('hello'); 98 | const body = document.body; 99 | 100 | const hasChildren = util.hasChildren(body); 101 | 102 | expect(hasChildren).to.be.equal(false); 103 | }); 104 | 105 | it('should have children when body is not empty', () => { 106 | const document = jsdom('
router
'); 107 | const body = document.body; 108 | 109 | const hasChildren = util.hasChildren(body); 110 | 111 | expect(hasChildren).to.be.equal(true); 112 | }); 113 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as util from './util'; 2 | 3 | /** 4 | * a very simple router for the **demo** of [weui](https://github.com/weui/weui) 5 | */ 6 | class Router { 7 | 8 | // default option 9 | _options = { 10 | container: '#container', 11 | enter: 'enter', 12 | enterTimeout: 0, 13 | leave: 'leave', 14 | leaveTimeout: 0 15 | }; 16 | 17 | _index = 1; 18 | 19 | // container element 20 | _$container = null; 21 | 22 | // array of route config 23 | _routes = []; 24 | 25 | // default route config 26 | _default = null; 27 | 28 | /** 29 | * constructor 30 | * @param options 31 | */ 32 | constructor(options) { 33 | this._options = Object.assign({}, this._options, options); 34 | this._$container = document.querySelector(this._options.container); 35 | } 36 | 37 | /** 38 | * initial 39 | * @returns {Router} 40 | */ 41 | init() { 42 | 43 | // why not `history.pushState`? see https://github.com/weui/weui/issues/26, Router in wechat webview 44 | window.addEventListener('hashchange', (event) => { 45 | const old_hash = util.getHash(event.oldURL); 46 | const hash = util.getHash(event.newURL); 47 | // fix '/' repeat see https://github.com/progrape/router/issues/21 48 | if(old_hash === hash) return; 49 | const state = history.state || {}; 50 | 51 | this.go(hash, state._index <= this._index); 52 | }, false); 53 | 54 | if (history.state && history.state._index) { 55 | this._index = history.state._index; 56 | } 57 | 58 | this._index--; 59 | 60 | const hash = util.getHash(location.href); 61 | const route = util.getRoute(this._routes, hash); 62 | this.go(route ? hash : this._default); 63 | 64 | return this; 65 | } 66 | 67 | /** 68 | * push route config into routes array 69 | * @param {Object} route 70 | * @returns {Router} 71 | */ 72 | push(route) { 73 | 74 | const exist = this._routes.filter(r => r.url === route.url)[0]; 75 | if (exist) { 76 | throw new Error(`route ${route.url} is existed`); 77 | } 78 | 79 | route = Object.assign({}, { 80 | url: '*', 81 | className: '', 82 | render: util.noop, 83 | bind: util.noop 84 | }, route); 85 | this._routes.push(route); 86 | return this; 87 | } 88 | 89 | /** 90 | * set default url when no matcher was found 91 | * @param {String} url 92 | * @returns {Router} 93 | */ 94 | setDefault(url) { 95 | this._default = url; 96 | return this; 97 | } 98 | 99 | /** 100 | * go to the specify url 101 | * @param {String} url 102 | * @param {Boolean} isBack, default: false 103 | * @returns {Router} 104 | */ 105 | go(url, isBack = false) { 106 | const route = util.getRoute(this._routes, url); 107 | if (route) { 108 | 109 | const leave = (hasChildren) => { 110 | // if have child already, then remove it 111 | if (hasChildren) { 112 | let child = this._$container.children[0]; 113 | if (isBack) { 114 | child.classList.add(this._options.leave); 115 | } 116 | 117 | if (this._options.leaveTimeout > 0) { 118 | setTimeout(() => { 119 | child.parentNode.removeChild(child); 120 | }, this._options.leaveTimeout); 121 | } 122 | else { 123 | child.parentNode.removeChild(child); 124 | } 125 | } 126 | }; 127 | 128 | const enter = (hasChildren, html) => { 129 | let node = document.createElement('div'); 130 | 131 | // add class name 132 | if (route.className) { 133 | node.classList.add(route.className); 134 | } 135 | 136 | node.innerHTML = html; 137 | this._$container.appendChild(node); 138 | // add class 139 | if (!isBack && this._options.enter && hasChildren) { 140 | node.classList.add(this._options.enter); 141 | } 142 | 143 | if (this._options.enterTimeout > 0) { 144 | setTimeout(() => { 145 | node.classList.remove(this._options.enter); 146 | }, this._options.enterTimeout); 147 | } 148 | else { 149 | node.classList.remove(this._options.enter); 150 | } 151 | 152 | location.hash = `#${url}`; 153 | try { 154 | isBack ? this._index-- : this._index++; 155 | history.replaceState && history.replaceState({_index: this._index}, '', location.href); 156 | } catch (e) { 157 | 158 | } 159 | 160 | if (typeof route.bind === 'function'/* && !route.__isBind*/) { 161 | route.bind.call(node); 162 | //route.__isBind = true; 163 | } 164 | }; 165 | 166 | const hasChildren = util.hasChildren(this._$container); 167 | 168 | // pop current page 169 | leave(hasChildren); 170 | 171 | // callback 172 | const callback = (err, html = '') => { 173 | if (err) { 174 | throw err; 175 | } 176 | 177 | // push next page 178 | enter(hasChildren, html); 179 | }; 180 | 181 | const res = route.render(callback); 182 | // promise 183 | if (res && typeof res.then === 'function') { 184 | res.then((html) => { 185 | callback(null, html); 186 | }, callback); 187 | } 188 | // synchronous 189 | else if (route.render.length === 0) { 190 | callback(null, res); 191 | } 192 | // callback 193 | else { 194 | 195 | } 196 | } 197 | else { 198 | throw new Error(`url ${url} was not found`); 199 | } 200 | return this; 201 | } 202 | } 203 | 204 | export default Router; -------------------------------------------------------------------------------- /dist/router.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * router v0.1.0 (https://github.com/progrape/router) 3 | * Copyright 2016 4 | * Licensed under the MIT license 5 | */ 6 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("Router",[],t):"object"==typeof exports?exports.Router=t():e.Router=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){"use strict";function r(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=e[n]);return t["default"]=e,t}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(t,"__esModule",{value:!0});var i=Object.assign||function(e){for(var t=1;t0?setTimeout(function(){e.parentNode.removeChild(e)},t._options.leaveTimeout):e.parentNode.removeChild(e)}()},i=function(o,i){var a=document.createElement("div");r.className&&a.classList.add(r.className),a.innerHTML=i,t._$container.appendChild(a),!n&&t._options.enter&&o&&a.classList.add(t._options.enter),t._options.enterTimeout>0?setTimeout(function(){a.classList.remove(t._options.enter)},t._options.enterTimeout):a.classList.remove(t._options.enter),location.hash="#"+e;try{n?t._index--:t._index++,history.replaceState&&history.replaceState({_index:t._index},"",location.href)}catch(u){}"function"==typeof r.bind&&r.bind.call(a)},a=s.hasChildren(t._$container);o(a);var u=function(e){var t=arguments.length<=1||void 0===arguments[1]?"":arguments[1];if(e)throw e;i(a,t)},l=r.render(u);l&&"function"==typeof l.then?l.then(function(e){u(null,e)},u):0===r.render.length&&u(null,l)}(),this}}]),e}();t["default"]=l,e.exports=t["default"]},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{"default":e}}function o(e){return e.indexOf("#")!==-1?e.substring(e.indexOf("#")+1):"/"}function i(e,t){for(var n=0,r=e.length;n0}function u(){}Object.defineProperty(t,"__esModule",{value:!0}),t.getHash=o,t.getRoute=i,t.hasChildren=a,t.noop=u;var s=n(2),l=r(s)},function(e,t,n){function r(e){for(var t,n=[],r=0,o=0,i="";null!=(t=y.exec(e));){var a=t[0],u=t[1],s=t.index;if(i+=e.slice(o,s),o=s+a.length,u)i+=u[1];else{var c=e[o],f=t[2],p=t[3],h=t[4],d=t[5],v=t[6],g=t[7];i&&(n.push(i),i="");var x=null!=f&&null!=c&&c!==f,_="+"===v||"*"===v,m="?"===v||"*"===v,w=t[2]||"/",b=h||d||(g?".*":"[^"+w+"]+?");n.push({name:p||r++,prefix:f||"",delimiter:w,optional:m,repeat:_,partial:x,asterisk:!!g,pattern:l(b)})}}return o