├── .babelrc ├── .browserslistrc ├── .eslintignore ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── .htmlhintrc ├── .npmignore ├── .npmrc_cn ├── LICENSE ├── README.md ├── config.js ├── dev_favicon.png ├── dev_fit.html ├── dev_index_jump.html ├── dist ├── history-navigation-vue.css ├── history-navigation-vue.js ├── history-navigation-vue.min.css └── history-navigation-vue.min.js ├── examples ├── app.js ├── config.js ├── create_app.js ├── css │ ├── docs.css │ └── transition-mask.css ├── index.ejs ├── pages │ ├── _inner-show.vue │ ├── api.vue │ ├── detail.vue │ ├── index.vue │ ├── index_modal.vue │ ├── list.vue │ ├── list_modal.vue │ ├── me.vue │ ├── modal.vue │ ├── nav.vue │ ├── not-found.vue │ ├── tra-performance-detail.vue │ └── tra-performance.vue ├── root.vue └── tip.vue ├── examples_vue3 ├── .npmrc ├── package-lock.json ├── package.json └── vue3_project │ ├── .gitignore │ ├── .npmrc │ ├── README.md │ ├── babel.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ └── index.html │ └── src │ ├── App.vue │ ├── assets │ └── logo.png │ ├── components │ └── HelloWorld.vue │ └── main.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── script ├── after_webpack_build.js ├── common.js ├── get-static-map.js ├── get_config.js └── pro.js ├── server ├── preview.js ├── preview_docs.js ├── preview_ex.js └── setup.js ├── src ├── bundle.js ├── cmpt │ ├── common.js │ ├── default-not-found.vue │ ├── navigation-controller.vue │ ├── navigator.vue │ ├── page.vue │ ├── page_main.vue │ ├── tab-bar-controller.vue │ └── tab-bar.vue ├── constant.js ├── css │ └── style.css ├── fit_vue.js ├── h_nav │ └── h_nav.js ├── install.js ├── mixin │ └── show-hide-mixin.js ├── navigator │ ├── history.js │ ├── libs │ │ ├── back.js │ │ ├── bae.js │ │ └── modal.js │ ├── native.js │ ├── navigator.js │ ├── state-key.js │ └── url.js └── util.js ├── ssr ├── conf.js ├── front-end │ ├── app.js │ └── entry-server.js ├── ssr-build.js ├── ssr-context-config.js ├── ssr-render.js └── webpack.config.js ├── test ├── e2e │ ├── .placeholder │ └── history.js └── unit │ └── util.js ├── webpack.build.config.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"], 3 | "plugins": [ 4 | "@babel/plugin-transform-runtime", 5 | "@babel/plugin-syntax-dynamic-import", 6 | "@babel/plugin-syntax-import-meta", 7 | "@babel/plugin-proposal-class-properties", 8 | "@babel/plugin-proposal-json-strings", 9 | [ 10 | "@babel/plugin-proposal-decorators", 11 | { 12 | "legacy": true 13 | } 14 | ], 15 | "@babel/plugin-proposal-function-sent", 16 | "@babel/plugin-proposal-export-namespace-from", 17 | "@babel/plugin-proposal-numeric-separator", 18 | "@babel/plugin-proposal-throw-expressions", 19 | "@babel/plugin-proposal-export-default-from", 20 | "@babel/plugin-proposal-logical-assignment-operators", 21 | "@babel/plugin-proposal-optional-chaining", 22 | [ 23 | "@babel/plugin-proposal-pipeline-operator", 24 | { 25 | "proposal": "minimal" 26 | } 27 | ], 28 | "@babel/plugin-proposal-nullish-coalescing-operator", 29 | "@babel/plugin-proposal-do-expressions", 30 | "@babel/plugin-proposal-function-bind" 31 | ], 32 | "env": { 33 | "production": { 34 | "plugins": [ 35 | ["transform-remove-console", { "exclude": [ "error", "warn"] }] 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | ie >= 10 2 | android > 4.4.4 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | webpack* 2 | node_modules 3 | script 4 | dist 5 | build 6 | server -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": ["eslint:recommended", "plugin:vue/recommended", "plugin:vue/strongly-recommended", "plugin:vue/essential"], 8 | 9 | "parserOptions": { 10 | "parser": "@babel/eslint-parser", 11 | "ecmaVersion": 2018, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "vue" 16 | ], 17 | "rules": { 18 | "indent": [ 19 | 0, 20 | 2, 21 | { "SwitchCase": 1 } 22 | ], 23 | "no-extra-semi": "warn", 24 | "no-unused-vars": "warn", 25 | "no-trailing-spaces": 0, 26 | "no-case-declarations": "warn", 27 | "no-unreachable": "warn", 28 | "no-multiple-empty-lines": 0, 29 | "prefer-const": 0, 30 | "space-infix-ops": "warn", 31 | "camelcase": 0, 32 | "no-console": 0, 33 | "linebreak-style": 0, 34 | "quotes": 0, 35 | "semi": 0, 36 | 37 | "vue/require-prop-types": 0, 38 | "vue/no-reserved-keys": 0, 39 | "vue/order-in-components": 0, 40 | "vue/require-default-prop": 0, 41 | "vue/return-in-computed-property": 0, 42 | "vue/max-attributes-per-line": 0, 43 | "vue/attributes-order": 0, 44 | "vue/singleline-html-element-content-newline": 0, 45 | "vue/mustache-interpolation-spacing": 0, 46 | "vue/html-self-closing": 0, 47 | "vue/html-indent": 0, 48 | "vue/multiline-html-element-content-newline": 0, 49 | "vue/no-multi-spaces": 0, 50 | "vue/html-quotes": 0, 51 | "vue/attribute-hyphenation": 0, 52 | "vue/html-closing-bracket-newline": 0, 53 | "vue/multi-word-component-names": 0, 54 | "vue/component-tags-order": 0, 55 | "vue/first-attribute-linebreak": 0 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | custom: ['https://www.paypal.me/hezedu'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.db 2 | /node_modules 3 | /examples_vue3/node_modules 4 | /examples_vue3/vue3_project/node_modules 5 | /package-lock.json 6 | /.vscode 7 | /.npmrc 8 | /dev_dist 9 | -------------------------------------------------------------------------------- /.htmlhintrc: -------------------------------------------------------------------------------- 1 | { 2 | "tagname-lowercase": true, 3 | "attr-lowercase": true, 4 | "attr-value-double-quotes": true, 5 | "doctype-first": false, 6 | "id-unique": true, 7 | "space-tab-mixed-disabled": "space" 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test.js 2 | /src 3 | /dist/dev 4 | /script 5 | /.babelrc 6 | /.eslintignore 7 | /.eslintrc.json 8 | /.htmlhintrc 9 | /plan.txt 10 | /static-server.js 11 | /webpack.config.js 12 | /index.html 13 | /.vscode 14 | /.setup.js 15 | -------------------------------------------------------------------------------- /.npmrc_cn: -------------------------------------------------------------------------------- 1 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 2 | phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ 3 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 4 | registry=https://registry.npm.taobao.org 5 | chromedriver_cdnurl=https://npm.taobao.org/mirrors/chromedriver 6 | 7 | detect_chromedriver_version=true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Du Wei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## History Navigation Vue 3 | The native-like Navigation for Web apps. 4 | 5 | ### Documentation 6 | To check out [live examples](https://hezedu.github.io/history-navigation-vue/examples.html) and docs, visit: 7 | 8 | [English docs](https://hezedu.github.io/history-navigation-vue/). 9 | 10 | [中文版文档](https://hezedu.github.io/history-navigation-vue/zh/) -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 8003, 3 | previewPort: 8004, 4 | previewDocsPort: 8005, 5 | previewExPort: 8006, 6 | vue3Port: 8007, 7 | title: 'hello' 8 | }; 9 | -------------------------------------------------------------------------------- /dev_favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezedu/history-navigation-vue/b68b1c59abb8dd4419d01cfc16ffc12b1cc9f311/dev_favicon.png -------------------------------------------------------------------------------- /dev_fit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 67 | hello 68 | 69 | 70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /dev_index_jump.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | hello 9 | 10 | 11 |

12 | 去首页 13 |

14 | 15 | -------------------------------------------------------------------------------- /dist/history-navigation-vue.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * history-navigation-vue v1.5.0 3 | * (c) 2021 hezedu 4 | * @license MIT 5 | */ 6 | .h-nav-ctrler{ 7 | width: 100%; 8 | height: 100%; 9 | overflow: hidden; 10 | position: relative; 11 | } 12 | 13 | .h-nav-page, .h-nav-tab-page { 14 | position: absolute; 15 | left: 0; 16 | top: 0; 17 | height: 100%; 18 | width: 100%; 19 | } 20 | 21 | .h-nav-page-main { 22 | background-color: #fff; 23 | position: relative; 24 | -webkit-overflow-scrolling: touch; 25 | overflow: auto; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | 30 | .h-nav-tabs-ctrler{ 31 | position: absolute; 32 | left: 0; 33 | top: 0; 34 | bottom: 0; 35 | right: 0; 36 | overflow: hidden; 37 | } 38 | 39 | 40 | 41 | .h-nav-page-handle, 42 | .h-nav-tab-page-handle{ 43 | width: 0!important; 44 | height: 0!important; 45 | border: 0!important; 46 | margin: 0!important; 47 | padding: 0!important; 48 | box-shadow: none!important; 49 | outline: none!important; 50 | position: static!important; 51 | } 52 | 53 | .h-nav-page-enter-active, 54 | .h-nav-page-leave-active, 55 | .h-nav-tab-page-enter-active, 56 | .h-nav-tab-page-leave-active{ 57 | transition: inherit; 58 | animation: inherit; 59 | } 60 | 61 | .h-nav-tab-pages-wrap { 62 | transition: inherit; 63 | animation: inherit; 64 | position: static!important; 65 | transition-property: none; 66 | animation-name: none; 67 | } 68 | 69 | .h-nav-tab-page-handle{ 70 | transition: inherit; 71 | animation: inherit; 72 | } 73 | .h-nav-tab-behavior-_inherit{ 74 | transition: inherit!important; 75 | animation: inherit!important; 76 | } 77 | 78 | .h-nav-modal { 79 | position: absolute; 80 | top: 0; 81 | left: 0; 82 | width: 100%; 83 | height: 100%; 84 | color: #fff; 85 | box-sizing: border-box; 86 | /* background-color: rgba(0, 0, 0, .5); */ 87 | } 88 | 89 | /* .h-nav-tab-page .h-nav-modal { 90 | z-index: 2000; 91 | } */ 92 | .h-nav-page-has-modal { 93 | overflow: hidden; 94 | } 95 | 96 | .h-nav-behavior-push > .h-nav-transition, 97 | .h-nav-behavior-back > .h-nav-transition, 98 | .h-nav-behavior-replace > .h-nav-transition{ 99 | transition: all .3s ease; 100 | } 101 | 102 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-enter, 103 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-leave-to { 104 | transform: translateX(100%); 105 | } 106 | 107 | .h-nav-behavior-replace > .h-nav-transition > .h-nav-page-enter { 108 | transform: translateY(100%); 109 | } 110 | 111 | /* ---------------- behavior push/back start ---------------- */ 112 | /* .h-nav-behavior-push > .h-nav-transition, 113 | .h-nav-behavior-back > .h-nav-transition, 114 | .h-nav-behavior-replace > .h-nav-transition{ 115 | transition: all .3s ease; 116 | } 117 | 118 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-enter, 119 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-leave-to, 120 | .h-nav-behavior-replace > .h-nav-transition > .h-nav-page-enter { 121 | transform: translateX(100%); 122 | } */ 123 | /* ---------------- behavior push/back end ---------------- */ 124 | 125 | .h-nav-tabbar{ 126 | box-sizing: border-box; 127 | border-top: 1px solid #c7c7c7; 128 | display: -ms-flexbox; 129 | display: flex; 130 | -ms-flex-pack: distribute; 131 | justify-content: space-around; 132 | background-color: #eee; 133 | } 134 | .h-nav-tabbar { 135 | height: 48px; 136 | position: absolute; 137 | bottom: 0; 138 | left: 0; 139 | width: 100%; 140 | z-index: 0; 141 | } 142 | .h-nav-tab-page { 143 | height: calc(100% - 48px); 144 | } 145 | .h-nav-tab-page > .h-nav-modal { 146 | height: calc(100% + 48px); 147 | } 148 | .h-nav-tab{ 149 | -ms-flex-positive: 1; 150 | flex-grow: 1; 151 | display: -ms-flexbox; 152 | display: flex; 153 | -ms-flex-align: center; 154 | align-items: center; 155 | -ms-flex-pack: center; 156 | justify-content: center; 157 | text-decoration: none; 158 | color: #333; 159 | -ms-flex-direction: column; 160 | flex-direction: column; 161 | } 162 | .h-nav-tab.h-nav-active{ 163 | color: #3eaf7c; 164 | } 165 | .h-nav-tab-icon{ 166 | font-size: 26px; 167 | width: 26px; 168 | height: 26px; 169 | line-height: 26px; 170 | text-align: center; 171 | background-repeat: no-repeat; 172 | background-size: contain; 173 | background-position: center center; 174 | margin-bottom: 4px; 175 | } 176 | .h-nav-tab-text{ 177 | font-size: 16px; 178 | line-height: 1; 179 | } 180 | .h-nav-tab-icon + .h-nav-tab-text{ 181 | font-size: 12px; 182 | } 183 | 184 | -------------------------------------------------------------------------------- /dist/history-navigation-vue.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * history-navigation-vue v1.5.0 3 | * (c) 2021 hezedu 4 | * @license MIT 5 | */ 6 | .h-nav-ctrler{height:100%;overflow:hidden;position:relative;width:100%}.h-nav-page,.h-nav-tab-page{height:100%;left:0;position:absolute;top:0;width:100%}.h-nav-page-main{-webkit-overflow-scrolling:touch;background-color:#fff;height:100%;overflow:auto;position:relative;width:100%}.h-nav-tabs-ctrler{bottom:0;left:0;overflow:hidden;position:absolute;right:0;top:0}.h-nav-page-handle,.h-nav-tab-page-handle{border:0!important;box-shadow:none!important;height:0!important;margin:0!important;outline:none!important;padding:0!important;position:static!important;width:0!important}.h-nav-page-enter-active,.h-nav-page-leave-active,.h-nav-tab-page-enter-active,.h-nav-tab-page-leave-active,.h-nav-tab-pages-wrap{animation:inherit;transition:inherit}.h-nav-tab-pages-wrap{animation-name:none;position:static!important;transition-property:none}.h-nav-tab-page-handle{animation:inherit;transition:inherit}.h-nav-tab-behavior-_inherit{animation:inherit!important;transition:inherit!important}.h-nav-modal{box-sizing:border-box;color:#fff;height:100%;left:0;position:absolute;top:0;width:100%}.h-nav-page-has-modal{overflow:hidden}.h-nav-behavior-back>.h-nav-transition,.h-nav-behavior-push>.h-nav-transition,.h-nav-behavior-replace>.h-nav-transition{transition:all .3s ease}.h-nav-behavior-back>.h-nav-transition>.h-nav-page-leave-to,.h-nav-behavior-push>.h-nav-transition>.h-nav-page-enter{transform:translateX(100%)}.h-nav-behavior-replace>.h-nav-transition>.h-nav-page-enter{transform:translateY(100%)}.h-nav-tabbar{-ms-flex-pack:distribute;background-color:#eee;border-top:1px solid #c7c7c7;bottom:0;box-sizing:border-box;display:-ms-flexbox;display:flex;height:48px;justify-content:space-around;left:0;position:absolute;width:100%;z-index:0}.h-nav-tab-page{height:calc(100% - 48px)}.h-nav-tab-page>.h-nav-modal{height:calc(100% + 48px)}.h-nav-tab{-ms-flex-positive:1;-ms-flex-align:center;-ms-flex-pack:center;align-items:center;color:#333;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;flex-grow:1;justify-content:center;text-decoration:none}.h-nav-tab.h-nav-active{color:#3eaf7c}.h-nav-tab-icon{background-position:50%;background-repeat:no-repeat;background-size:contain;font-size:26px;height:26px;line-height:26px;margin-bottom:4px;text-align:center;width:26px}.h-nav-tab-text{font-size:16px;line-height:1}.h-nav-tab-icon+.h-nav-tab-text{font-size:12px} -------------------------------------------------------------------------------- /dist/history-navigation-vue.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * history-navigation-vue v1.5.0 3 | * (c) 2021 hezedu 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.historyNavigationVue=e():t.historyNavigationVue=e()}(self,(function(){return function(){"use strict";var t={d:function(e,a){for(var i in a)t.o(a,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:a[i]})},o:function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r:function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}},e={};t.r(e),t.d(e,{plugin:function(){return Q},version:function(){return Z}});var a="_h_nav_page_show",i="_h_nav_page_hide",n="h-nav-page-",s="h-nav-page-not-found",o="_h_n_key",r="_h_n_modal_key",h="_H_NAV_BAE_MODAL_";function l(t,e,a,i,n,s,o,r){var h,l="function"==typeof t?t.options:t;if(e&&(l.render=e,l.staticRenderFns=a,l._compiled=!0),i&&(l.functional=!0),s&&(l._scopeId="data-v-"+s),o?(h=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),n&&n.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(o)},l._ssrRegister=h):n&&(h=r?function(){n.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:n),h)if(l.functional){l._injectStyles=h;var c=l.render;l.render=function(t,e){return h.call(e),c(t,e)}}else{var u=l.beforeCreate;l.beforeCreate=u?[].concat(u,h):[h]}return{exports:t,options:l}}var c,u,p,_=l({name:"HistoryNavigationPage",provide:function(){return{$page:this}},props:{path:{type:String},title:String,isHome:Boolean,isTab:{type:Boolean,required:!0},tabIndex:Number,stateKey:Number,route:{type:Object,required:!0},isFirstLoad:{type:Boolean,required:!0},isActive:{type:Boolean,required:!0}},data:function(){return{isLoad:this.isFirstLoad}},watch:{isActive:function(){this._handleShowHide()}},methods:{_handleShowHide:function(){this.isActive?this._uniteEmit(a):this._uniteEmit(i)},_uniteEmit:function(t){this.$emit(t)},_uniteBeforeDestory:function(){this.isLoad&&(this.$options._tmp_is_before_destroy=!0,this.isActive&&this._uniteEmit(i),this.isLoad=!1)}},beforeCreate:function(){},mounted:function(){var t=this;this.isLoad?this._uniteEmit(a):setTimeout((function(){t.$options._tmp_is_before_destroy||(t.isLoad=!0,t.$nextTick((function(){t._uniteEmit(a)})))}))},beforeDestroy:function(){this._uniteBeforeDestory()},beforeUnmount:function(){this._uniteBeforeDestory()}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"h-nav-page-main"},[t._t("default")],2)}),[],!1,null,null,null),d=l({name:"HistoryNavigationPageWrap",components:{PageMain:_.exports},props:{transitionName:{type:String,default:"h-nav-page"},v:{type:Object,required:!0},isActive:{type:Boolean,required:!0},isFirstLoad:{type:Boolean,required:!0},globalPageStyle:void 0}},(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("transition",{attrs:{name:t.transitionName,appear:!0}},[a("div",{directives:[{name:"show",rawName:"v-show",value:t.isActive,expression:"isActive"}],class:t.transitionName,style:{zIndex:t.v.stateKey}},[a("PageMain",{class:t.v.className,style:[t.globalPageStyle,t.v.style],attrs:{path:t.v.path,title:t.v.title,isHome:t.v.isHome,isTab:t.v.isTab,tabIndex:t.v.tabIndex,stateKey:t.v.stateKey,route:t.v.route,isFirstLoad:t.isFirstLoad,isActive:t.isActive}},[a(t.v.cmptKey,{tag:"component"})],1),t._v(" "),t._l(t.v.modalList,(function(t){return a("div",{directives:[{name:"show",rawName:"v-show",value:!t.isBAE,expression:"!modal.isBAE"}],key:t.key,staticClass:"h-nav-modal",style:{zIndex:t.key}},[a("div",{attrs:{id:"h_nav_modal_"+t.uid}})])}))],2)])}),[],!1,null,null,null),v=d.exports,f=l({props:{list:{type:Array,required:!0},currentIndex:{type:Number,required:!0}},name:"HistoryNavigationTabBar"},(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticClass:"h-nav-tabbar"},t._l(t.list,(function(e,i){return a("Navigator",{key:e.pagePath,staticClass:"h-nav-tab",attrs:{isActive:t.currentIndex===i,url:e.pagePath,type:"switchTab"}},[e.icon?a("div",{staticClass:"h-nav-tab-icon",class:e.icon}):t._e(),t._v(" "),a("div",{staticClass:"h-nav-tab-text"},[t._v(t._s(e.text))])])})),1)}),[],!1,null,null,null),b=l({name:"HistoryNavigationTabBarController",components:{Page:v,TabBar:f.exports},props:{currTabPage:{type:Object,required:!0},isActive:{type:Boolean,required:!0},isFirstLoad:{type:Boolean,required:!0},traClassName:{type:String},globalPageStyle:void 0,tabList:Array,tabStackMap:Object},data:function(){return{tabBehavior:"_inherit",tabBehaviorDistance:0}},watch:{"currTabPage.tabIndex":function(t,e){var a=t-e,i=a>0?"increase":"reduce";this.tabBehaviorDistance=a,this.tabBehavior=i},isActive:function(){this.tabBehavior="_inherit"}},created:function(){}},(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("transition",{attrs:{name:"h-nav-page",appear:!0}},[a("div",{directives:[{name:"show",rawName:"v-show",value:t.isActive,expression:"isActive"}],staticClass:"h-nav-tabs-ctrler h-nav-tabs-wrap"},[a("transition-group",{staticClass:"h-nav-tab-pages-wrap",class:"h-nav-tab-behavior-"+t.tabBehavior,style:{"--h-nav-tab-distance":t.tabBehaviorDistance},attrs:{tag:"div","enter-class":"","leave-class":"","enter-to-class":"","leave-to-class":"","enter-active-class":"h-nav-tab_page_load","leave-active-class":"h-nav-tab_page_unload"}},t._l(t.tabStackMap,(function(e){return a("div",{key:e.tabIndex,staticClass:"h-nav-tab-page-handle",class:t.traClassName,style:e.isClean?"transition: none!important; animation: none!important;":""},[a("Page",{attrs:{transitionName:"h-nav-tab-page",v:e,isActive:t.isActive&&t.currTabPage.tabIndex===e.tabIndex,isFirstLoad:t.isFirstLoad,globalPageStyle:t.globalPageStyle}})],1)})),0),t._v(" "),a("TabBar",{attrs:{list:t.tabList,currentIndex:t.currTabPage.tabIndex}})],1)])}),[],!1,null,null,null),y=l({components:{Page:v,TabCtrler:b.exports},name:"HistoryNavigationController",props:{entryPagePath:{type:String}},data:function(){var t=this.$navigator._h;return{isErr:!1,isFirstLoad:!0,stackMap:t.stackMap,behavior:t.behavior,currentPage:t.currentPage,globalPageStyle:this.$navigator.GLOBAL_CONFIG.pageStyle,transition:t._tra,tabStackMap:t.tabStackMap,tabList:t.tabList}},methods:{_uniteDestroy:function(){this.$navigator._h.destroy()}},created:function(){var t=this,e=this.$navigator._h;e.checkCompatibility()?(e._load(this.entryPagePath),this.$nextTick((function(){t.isFirstLoad=!1}))):this.isErr=!0},destroyed:function(){this._uniteDestroy()},unmounted:function(){this._uniteDestroy()}},(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("transition-group",{staticClass:"h-nav-ctrler",class:"h-nav-behavior-"+t.behavior.type,style:{"--h-nav-distance":t.behavior.distance},attrs:{tag:"div","enter-class":"","leave-class":"","enter-to-class":"","leave-to-class":"","enter-active-class":"h-nav--page_load","leave-active-class":"h-nav--page_unload"}},[t._l(t.stackMap,(function(e){return a("div",{key:e.stackId,staticClass:"h-nav-page-handle",class:t.transition.className,style:e.isClean?"transition: none!important; animation: none!important;":""},[e.isTab?a("TabCtrler",{key:e.stackId,style:[{zIndex:e.stateKey}],attrs:{tabStackMap:t.tabStackMap,tabList:t.tabList,traClassName:t.transition.className,currTabPage:e,isActive:e.stackId===t.currentPage.stackId,isFirstLoad:t.isFirstLoad,globalPageStyle:t.globalPageStyle}}):a("Page",{key:e.stackId,attrs:{transitionName:"h-nav-page",v:e,isActive:e.stackId===t.currentPage.stackId,isFirstLoad:t.isFirstLoad,globalPageStyle:t.globalPageStyle}})],1)})),t._v(" "),t.isErr?a("div",{key:"not_support",staticClass:"h-nav-not-support"},[t._t("default",[a("h1",[t._v("history-navigation-vue")]),t._v(" "),a("p",[t._v("Sorry, Your browser doesn't support HTML history API.")])])],2):t._e()],2)}),[],!1,null,null,null).exports,g=l({data:function(){return{homePagePath:this.$navigator.GLOBAL_CONFIG.homePagePath}}},(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{staticStyle:{"padding-top":"8%","text-align":"center"}},[a("h1",[t._v("History Navigation Vue")]),t._v(" "),a("h2",[t._v("Page Not Found")]),t._v(" "),a("br"),t._v(" "),t.$page.stateKey>1?a("navigator",{staticStyle:{"text-decoration":"underline"},attrs:{type:"back"}},[t._v("Back")]):t._e(),t._v(" "),a("br"),t._v(" "),a("br"),t._v(" "),a("navigator",{staticStyle:{"text-decoration":"underline"},attrs:{type:"relaunch",url:t.homePagePath}},[t._v("Take me home")])],1)}),[],!1,null,null,null).exports,m=Object.assign(Object.create(null),{push:!0,replace:!0,relaunch:!0,switchTab:!0}),k=l({name:"HistoryNavigator",props:{url:{type:String},type:{type:String,default:"push"},steps:{type:Number,default:1},transition:{type:String},isActive:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},event:{type:String}},computed:{href:function(){return this.url?this.$navigator.URL.toLocationUrl(this.url):void 0}},methods:{handleEvent:function(t){if(t.preventDefault(),!this.isActive&&!this.disabled)if("back"===this.type)this.$navigator.back(this.steps,this.transition);else m[this.type]?this.$navigator[this.type]({url:this.url,transition:this.transition}):console.error("history-navigation-vue navigator not support type: "+this.type)}},mounted:function(){var t=this.event||this.$navigator.GLOBAL_CONFIG.navigatorTriggerEvent;this.$el.addEventListener(t,this.handleEvent)}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("a",{class:{"h-nav-active":t.isActive,"h-nav-disabled":t.disabled},attrs:{href:t.href}},[t._t("default")],2)}),[],!1,null,null,null).exports;function P(t,e,a){return e in t?Object.defineProperty(t,e,{value:a,enumerable:!0,configurable:!0,writable:!0}):t[e]=a,t}function T(t){var e=t.isHashMode,a=void 0===e||e,i=t.base,n=void 0===i?"":i;this.isHashMode=a,this._location=p,a?this.hashBase=this._location.pathname+this._location.search+"#":this.base=n}function A(t){var e,a,i=t.indexOf("?");return-1!==i?(e=t.substr(0,i),a=t.substr(i+1)):(e=t,a=""),{path:e,query:S(a)}}function B(t){var e,a,i;return"string"==typeof t?(e=A(t),a=t):a=M(e=C(t)),i=I(e.path),Object.assign({fullPath:a,trimedPath:i},e)}function C(t){if(-1===t.path.indexOf("?"))return t;var e=A(t.path);return Object.assign(e.query,t.query),e}function M(t){var e=function(t){if(!t)return"";var e=[];return Object.keys(t).forEach((function(a){e.push(a+"="+encodeURIComponent(t[a]))})),e.join("&")}(t.query);return e&&(e="?"+e),t.path+e}function S(t){var e=Object.create(null);if(!t||"string"==typeof t&&0===t.length)return e;for(var a,i=t.split("&"),n=0,s=i.length;n0&&(t=t.substr(1)),t},T.prototype.getUrlByLocation=function(){if(this.isHashMode)return this.getHashUrlByLocation();var t=this._location.pathname;return 0===t.indexOf(this.base)&&(t=t.substr(this.base.length)),t+this._location.search},T.prototype.getRouteByLocation=function(){return A(this.getUrlByLocation())},T.prototype.toLocationUrl=function(t){return this.isHashMode?this.hashBase+t:this.base+t};var w=T;function L(){var t=u.state;return t&&"number"==typeof t._h_n_key?t._h_n_key:1}function E(){var t=u.state;return t&&"number"==typeof t._h_n_modal_key?t._h_n_modal_key:0}function x(){return{key:L(),modalKey:E()}}var O=x();function N(){return Object.assign({},O)}function $(){var t;t=x(),O=Object.assign({},t)}function j(){}function R(t,e,a,i){var n=e[a];t[a]=void 0===n?i:n}function H(t){throw new Error("history-navigation-vue: ".concat(t))}function F(t,e,a){var i=function(t,e,a){var i=U(t,e.key),n=U(t,a.key),s=0,o=t[i];for(o&&o[0]===e.key&&(s=t[i][1]-e.modalKey),s+=a.modalKey,i+=1;i=e);a++);return a}var K={init:function(){this._modal_crumbs=null,this._initModalCrumbs()},proto:{modal:function(t){var e,a=this,i=t.component,n=t.propsData,s=t.parent,l=t.success,c=arguments[0]===h;c||this._autoBAE();var u=L(),p=this.stackMap[u],_=E();_+=1,this._history.pushState((P(e={},o,u),P(e,r,_),e),""),this._setModalCrumbsWhenChange(),$();var d={key:_,uid:q()};if(p.modalList.push(d),!c){var v="h_nav_modal_"+d.uid;return i&&this.uniteVue.nextTick((function(){if(!d._isDestroy){var t=a.uniteVue.newComponent(i,{el:"#"+v,parent:s,propsData:n});d._destoryCmpt=function(){a.uniteVue.destroy(t)},l&&l(t)}})),v}d.isBAE=!0},removeModal:function(){var t,e=this,a=L(),i=E(),n=this.stackMap[a];n&&(t=n.modalList.splice(i)).forEach((function(t){t._isDestroy=!0,t._destoryCmpt&&t._destoryCmpt()})),0===i&&this._isNeedBAE()&&(t&&t.length>1?this._autoBAE():(this.BAE.onFirstTrigger(),setTimeout((function(){e._autoBAE()}),this.BAE.maxInterval)))},_autoRemoveModal:function(){var t=L(),e=this.stackMap[t],a=E();e&&e.modalList.length>a&&this.removeModal()},_setModalCrumbsWhenChange:function(){var t=L(),e=E();this._setModalCrumbs(t,e)},_initModalCrumbs:function(){var t=this._history,e=Object.assign({},t.state),a=e._h_n_crumbs;if(a)delete e._h_n_crumbs,this._history.replaceState(e,""),this._modal_crumbs=function(t){for(var e,a=t.split(";"),i=[],n=0,s=a.length;n=e);i++);t.splice(i,t.length),a&&t.push([e,a])}(this._modal_crumbs,t,e)},_saveModalCrumbs:function(){if(this._modal_crumbs.length){var t=this._history.state||{};t._h_n_crumbs=this._modal_crumbs.map((function(t){return t.join(":")})).join(";"),this._history.replaceState(t,"")}},getLastModalKeyByCrumbs:function(t){var e=this._modal_crumbs,a=e[e.length-1];return a&&a[0]===t?a[1]:0}}};var V=0;function q(){return V+=1}var D={proto:{_isBAEPage:function(){if(this.BAE){var t=L(),e=this.stackMap[t];if(1===t&&e&&(e.isHome||e.isTab))return!0}return!1},_isBAEPageByTK:function(t){if(this.BAE){var e=L(),a=this.pageMap[t];if(1===e&&a&&(a.isHome||a.isTab))return!0}return!1},_isNeedBAE:function(){if(this._isBAEPage()){var t=this._history.state;if(t&&!t._h_n_modal_key)return!0}return!1},_autoBAE:function(){this._isNeedBAE()&&this.modal(h)}}},W={init:function(){this._whenPopTra=null,this._whenPopInfo=null,this._whenBackPopInfo=null},proto:{_getStepsTotal:function(t){var e=x(),a=e.key,i=e.modalKey,n=t.key,s=t.modalKey,o=a-n;if(0===o)return i-s;var r,h,l=o>0;l?(r=t,h=e):(r=e,h=t);var c=F(this._modal_crumbs,r,h);return l||(c=-c),c},_backGetTo1Count:function(){return this._getStepsTotal({key:1,modalKey:0})},back:function(t,e){var a=t||1;a<1||(this._whenPopTra=e,a&&this._history.go(-a))},backToPage:function(t,e){var a=this._modal_crumbs;if(a.length){for(var i,n=L()-t,s=0,o=0,r=a.length;sn;s++)o+=i[1];this.back(t+o,e)}else this.back(t,e)},_backAndApply:function(t,e,a,i){t<1||(this._whenBackPopInfo={method:e,args:a},this.back(t,i))},_backToStartAndReplace:function(t,e,a){var i=this._backGetTo1Count();i>0?this._backAndApply(i,"_replace",[t,e],a):0===i?(this._setTra(a),this._replace(t,e)):console.error("_backToStartAndReplace not back",i)}}},G=!1;function z(t){G&&H("Only one instance can be generated."),G=!0,this._global=t.global,this._window=c,this._history=u,this._location=p,this._isOmitForwardEvent=!1,this.BAE=t.BAE,this._tra={className:this._global.transition},this.uniteVue=t.uniteVue,this.pageMap=t.pageMap,this.notFoundPage=t.notFoundPage,t.tabBar&&(this.tabMap=t.tabBar.map,this.tabList=t.tabBar.list,this.tabStackMap=Object.create(null)),this.stackMap=Object.create(null),this._stackItemId=1,this.tabCtrlerStackId=1,this.onRouted=t.onRouted||j,this.URL=new w({isHashMode:t.urlIsHashMode,base:t.urlBase}),this.behavior={type:"",distance:0,isPop:!1},this.currentPage={path:null,title:null,className:void 0,isHome:!1,isTab:!1,tabIndex:null,cmptKey:null,stackId:null,stateKey:null,modalList:[],isClean:!1,route:{}},W.init.apply(this),K.init.apply(this)}z.prototype.checkCompatibility=function(){return!(!this._history||!this._history.pushState)},z.prototype._bind=function(){var t=this;this._popstateHandle=function(){t.handlePop()},this._window.addEventListener("popstate",this._popstateHandle),this._handleWinUnload=function(){t.handleWinUnload()},this._window.addEventListener("beforeunload",this._handleWinUnload)},z.prototype._onRouted=function(){var t=this.currentPage;this.onRouted({title:t.title,routeFullPath:t.route.fullPath}),this._autoBAE(t.route.trimedPath)},z.prototype._genStackItemId=function(){return this._stackItemId=this._stackItemId+1,"stack_"+this._stackItemId},z.prototype._isTabRoute=function(t){return!!this.tabMap&&!!this.tabMap[t]},z.prototype._forMatInputArg=function(t){var e,a;return"string"==typeof t?e=B(t):(e=B({path:t.url,query:t.query}),a=t.transition),{fullParse:e,tra:a}},z.prototype._load=function(t){this._bind();var e=B(void 0===t?this.URL.getUrlByLocation():t);1!==L()&&this._isTabRoute(e.trimedPath)?this._backToStartAndReplace(e,"loaded"):this._replaceCurrPage(e,"loaded")},z.prototype._replaceCurrPage=function(){var t=E();t?this._backAndApply(t,"_replace",arguments):this._replace.apply(this,arguments)},z.prototype._setTra=function(t){this._tra.className=t||this._global.transition},z.prototype.push=function(t){var e=this._forMatInputArg(t),a=e.fullParse,i=e.tra;this._isTabRoute(a.trimedPath)?console.error("Cannot push the tab url, please use switchTab"):(this._setTra(i),this._push(a,i))},z.prototype.replace=function(t){var e=this._forMatInputArg(t),a=e.fullParse,i=e.tra;this._isTabRoute(a.trimedPath)?console.error("Cannot replace the tab url, please use switchTab"):(this._setTra(i),this._replaceCurrPage(a))},z.prototype.switchTab=function(t){var e=this._forMatInputArg(t),a=e.fullParse,i=e.tra;this._isTabRoute(a.trimedPath)?this._backToStartAndReplace(a,"switchtab",i):console.error(a," is not tab url")},z.prototype.relaunch=function(t){var e=this._forMatInputArg(t),a=e.fullParse,i=e.tra;this.tabCtrlerStackId=this.tabCtrlerStackId+1,this._backToStartAndReplace(a,"relaunch",i)},z.prototype._setMapItem=function(t,e){var a=this.pageMap[e.trimedPath]||this.notFoundPage,i={path:a.path,title:a.title,tabIndex:a.tabIndex,route:e,cmptKey:a.cmptKey,isHome:a.isHome,isTab:a.isTab,stateKey:t,className:a.className,style:a.style,modalList:[],isClean:!1};i.isTab?(i.stackId="tab_stack_"+this.tabCtrlerStackId,this.uniteVue.set(this.tabStackMap,a.tabIndex,i)):i.stackId=this._genStackItemId(),this.uniteVue.set(this.stackMap,t,i),Object.assign(this.currentPage,i)},z.prototype._getBackTra=function(){var t=this._history;if(t.state)return t.state._h_n_b_tra},z.prototype._push=function(t,e){if(this._autoBAE(),e!==this._getBackTra()){var a=Object.assign({},this._history.state);e?a._h_n_b_tra=e:delete a._h_n_b_tra,this._history.replaceState(a,"")}var i=L()+1;this._setModalCrumbsWhenChange(),this._history.pushState(P({},o,i),"",this.URL.toLocationUrl(t.fullPath)),$();Object.assign(this.behavior,{type:"push",distance:1,isPop:!1}),this._setMapItem(i,t),this._onRouted()},z.prototype._replace=function(t,e){var a=this,i=N().key,n=L(),s=n-i,o={type:e||"replace",distance:s,isPop:!1};Object.assign(this.behavior,o),("switchtab"!==this.behavior.type||s)&&(this.currentPage.stackId="unactive_"+this.currentPage.stackId);var r=this.URL.toLocationUrl(t.fullPath),h=this._history.state;h&&h._h_n_key||((h=Object.assign({},history.state))._h_n_key=n),this._history.replaceState(h,"",r),$(),this.uniteVue.nextTick((function(){if("relaunch"===o.type){a._setAllCleaned();var e=n-s;a.stackMap[e].isClean=!1,a.uniteVue.nextTick((function(){a._clearAll(),a._setMapItem(n,t),a._onRouted()}))}else a._clearAfter(),a._setMapItem(n,t),a._onRouted()}))},z.prototype._setMapCleaned=function(t){for(var e in t)t[e].isClean=!0},z.prototype._setAllCleaned=function(){this.tabStackMap&&this._setMapCleaned(this.tabStackMap),this._setMapCleaned(this.stackMap)},z.prototype._clearAfter=function(){var t,e,a=this,i=L(),n=this.stackMap,s=[];for(t in n)(e=n[t]).stateKey>i&&(e.isClean=!0,s.push(e));var o=s.length;if(o){var r=s.pop();r.isClean=!1,this.uniteVue.delete(n,r.stateKey),(o=s.length)&&this.uniteVue.nextTick((function(){for(t=0;t0)return console.error("Forward is disabled by history-navigation-vue"),this._isOmitForwardEvent=!0,void this.back(s);var r=x(),h=r.key,l=this.stackMap[h];if(l||!r.modalKey){if(this._autoRemoveModal(),$(),i!==h){var c=h-i,u=this._whenPopTra;u||-1!==c||(u=this._getBackTra()),this._setTra(u),this._whenPopTra=null;var p={type:"back",distance:c,isPop:!0};Object.assign(this.behavior,p),l?Object.assign(this.currentPage,l):this._setMapItem(h,B(this.URL.getUrlByLocation())),this.uniteVue.nextTick((function(){t._clearAfter(),t._onRouted()}))}}else this.back(r.modalKey)}},z.prototype.destroy=function(){G&&(this._popstateHandle&&this._window.removeEventListener("popstate",this._popstateHandle),this._handleWinUnload&&this._window.removeEventListener("_handleWinUnload",this._handleWinUnload),G=!1)},z.prototype.handleWinUnload=function(){this._saveModalCrumbs()},Object.assign(z.prototype,K.proto),Object.assign(z.prototype,D.proto),Object.assign(z.prototype,W.proto);var X=z;function Y(t){return P({inject:{$page:{default:null}},created:function(){(this.$options.onShow||this.$options.onHide)&&this.$page&&function(t){var e,n=t.$options;n.__tmp_h_nav_show_hide_has=!0,n.onShow&&(e=n.__tmp_h_nav_page_show_hanlde=function(){n.onShow.call(t)},t.$page.$on(a,e));n.onHide&&(e=n.__tmp_h_nav_page_hide_hanlde=function(){n.onHide.call(t)},t.$page.$on(i,e))}(this)}},t?"unmounted":"destroyed",(function(){this.$options.__tmp_h_nav_show_hide_has&&function(t){var e=t.$options,n=e.__tmp_h_nav_page_show_hanlde;n&&t.$page.$off(a,n);(n=e.__tmp_h_nav_page_hide_hanlde)&&t.$page.$off(i,n)}(this)}))}var J={title:"Not Found",component:g};var Q={install:function(t,e){var a,i=function(t){var e=t.version;return e=e.substr(0,e.indexOf(".")),Number(e)}(t);2===i?a=function(t){return{proto:t.prototype,nextTick:t.nextTick,newComponent:function(e,a){return new(t.extend(e))(a)},destroy:function(t){t.$destroy()},set:t.set,delete:t.delete}}(t):H("Unsupported version of Vue "+i),Array.isArray(e.pages)||H("config.pages is not Array.");var o,r=function(t){for(var e,a,i,s=Object.create(null),o=0,r=t.length;o= 2");for(var n,s,o=0,r=Object.create(null),h=[];o { 65 | isAgain = false; 66 | }, interval); 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /examples/create_app.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | import config from './config'; 4 | 5 | 6 | 7 | // import '../dist/history-navigation-vue.css'; 8 | // import * as hNav from '../dist/history-navigation-vue'; 9 | import * as hNav from '../src/bundle'; 10 | import './css/docs.css'; 11 | import './css/transition-mask.css'; 12 | 13 | import Root from './root.vue'; 14 | 15 | export default function createApp(_Vue, _config){ 16 | if(_config){ 17 | Object.assign(config, _config); 18 | } 19 | _Vue.use(hNav.plugin, config); 20 | var app = new _Vue({ 21 | render: h => h(Root) 22 | }); 23 | app.$mount('#app'); 24 | return app; 25 | } -------------------------------------------------------------------------------- /examples/css/docs.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | *{ 4 | box-sizing: border-box; 5 | } 6 | html, body { 7 | height: 100%; 8 | } 9 | 10 | body, h1, h2, ul, li, button, pre{ 11 | padding: 0; 12 | margin: 0; 13 | } 14 | body { 15 | background-color: #ddd; 16 | } 17 | 18 | .h-nav-ctrler{ 19 | max-width: 768px; 20 | margin: 0 auto; 21 | } 22 | h1{ 23 | font-size: 2em; 24 | margin-bottom: 1em; 25 | margin-top: 1em; 26 | } 27 | h2{ 28 | font-size: 1.5em; 29 | margin-bottom: 1em; 30 | margin-top: 1em; 31 | } 32 | body{ 33 | font-size: 16px; 34 | } 35 | input { 36 | line-height: normal; 37 | } 38 | div:focus{ 39 | outline: none; 40 | } 41 | ul { 42 | list-style: none; 43 | } 44 | form{ 45 | display: block; 46 | } 47 | .root{ 48 | min-height: 100vh; 49 | } 50 | .page{ 51 | min-height: 200vh; 52 | background-image: linear-gradient(#eee, #999, #eee, #999); 53 | } 54 | a{ 55 | text-decoration: none; 56 | cursor: pointer; 57 | color: #333; 58 | } 59 | 60 | .wrap{ 61 | max-width: 1000px; 62 | margin: 0 auto; 63 | position: relative; 64 | } 65 | .bottom_bar{ 66 | height: 50px; 67 | position: fixed; 68 | left: 0; 69 | bottom: 0; 70 | width: 100%; 71 | background-color: #ccc; 72 | border-top: 1px solid #fff; 73 | display: flex; 74 | } 75 | a.bottom_bar_item{ 76 | width: 50%; 77 | flex-shrink: 0; 78 | text-align: center; 79 | line-height: 49px; 80 | color: #333; 81 | } 82 | a.bottom_bar_item.router-link-exact-active{ 83 | color: #00008B; 84 | font-weight: bold; 85 | } 86 | .index_to_list{ 87 | padding-top: 150px; 88 | text-align: center; 89 | display: block; 90 | } 91 | 92 | .item{ 93 | display: flex; 94 | align-items: center; 95 | height: 35px; 96 | justify-content: space-around; 97 | padding: 0 10px; 98 | background-color: #fff; 99 | border-bottom: 1px solid #ddd; 100 | line-height: 1; 101 | } 102 | .item_active{ 103 | background-color: #ddd; 104 | } 105 | 106 | 107 | 108 | body{ 109 | font-size: 16px; 110 | } 111 | 112 | .index_page{ 113 | min-height: 100vh; 114 | } 115 | 116 | .index_logo{ 117 | width: 2em; 118 | height: 2em; 119 | display: block; 120 | } 121 | .index_page h1, .index_page h2 { 122 | text-align: center; 123 | } 124 | .index_desc{ 125 | color: #999; 126 | margin-top: 0; 127 | } 128 | .nav{ 129 | 130 | width: 100%; 131 | display: flex; 132 | align-items: center; 133 | padding-top: 8px; 134 | } 135 | .nav a { 136 | margin: 0 1em; 137 | } 138 | .nav .gitgub{ 139 | width: 30px; 140 | height: 30px; 141 | } 142 | 143 | .index_page h2 b{ 144 | color: #000; 145 | } 146 | 147 | a { 148 | cursor: pointer; 149 | } 150 | .h-nav-active{ 151 | opacity: 1; 152 | } 153 | .api_nav.h-nav-active{ 154 | color: #000; 155 | font-weight: bold; 156 | } 157 | 158 | 159 | 160 | 161 | 162 | .index_table_wrap{ 163 | display: flex; 164 | align-items: center; 165 | justify-content: center; 166 | flex-direction: column; 167 | } 168 | 169 | .index_table{ 170 | border-collapse: collapse; 171 | border-color: #ccc; 172 | border: 0; 173 | } 174 | 175 | .index_table th{ 176 | background-color: #eee; 177 | } 178 | 179 | .index_table th, .index_table td{ 180 | padding: 3px 12px; 181 | } 182 | 183 | td.index_table_f{ 184 | padding-right: 45px; 185 | font-weight: bold; 186 | } 187 | 188 | .index_table i{ 189 | color: #999; 190 | } 191 | 192 | 193 | .index_diff_loading{ 194 | cursor: auto; 195 | } 196 | 197 | .index_diff_loading::before { 198 | padding:0; 199 | margin:0; 200 | position: absolute; 201 | top:0; 202 | bottom:0; 203 | right:0; 204 | left:0; 205 | margin:auto; 206 | height: 13px; 207 | width: 13px; 208 | animation: rotate 0.8s infinite linear; 209 | border: 3px solid #999; 210 | border-right-color: #fff; 211 | border-radius: 50%; 212 | } 213 | 214 | .index_diff_loading::before{ 215 | content: ''; 216 | } 217 | 218 | @keyframes rotate { 219 | 0% { transform: rotate(0deg); } 220 | 100% { transform: rotate(360deg); } 221 | } 222 | 223 | .index_diff_loading_more .index_diff_loading::before{ 224 | width: 9px; 225 | height: 9px; 226 | border-width: 2px; 227 | } 228 | .index_diff_start_link{ 229 | text-decoration: underline; 230 | padding: 3px 10px; 231 | border-radius: 5px; 232 | color: #333; 233 | } 234 | 235 | .index_diff_list_item_active, 236 | .index_diff_page_back_active, 237 | .index_diff_start_active{ 238 | transition: all .15s; 239 | background-color: #aaa; 240 | color: #fff; 241 | } 242 | .index_diff_start_active{ 243 | text-decoration: none; 244 | } 245 | .index_codepen_wrap{ 246 | width: 980px; 247 | margin: 0 auto; 248 | height: 565px; 249 | } 250 | 251 | footer{ 252 | padding: 25px 0px; 253 | line-height: 1.7em; 254 | background-color: #eee; 255 | text-align: center; 256 | font-size: 14px; 257 | } 258 | 259 | .index_get_start{ 260 | text-align: center; 261 | font-size: 30px; 262 | margin-top: 20px; 263 | padding-bottom: 50px; 264 | } 265 | 266 | .api_wrap h1{ 267 | text-align: center; 268 | } 269 | 270 | .red{ 271 | height: 100px; 272 | width: 100px; 273 | 274 | } 275 | 276 | .index_icon{ 277 | background-image: url('https://www.w3school.com.cn/ui2017/compatible_firefox.png'); 278 | } 279 | .h-nav-actived > .index_icon{ 280 | background-image: url('https://www.w3school.com.cn/ui2017/compatible_ie.png'); 281 | } 282 | 283 | 284 | /* .h-nav-tab-page { 285 | top: 48px; 286 | } 287 | .h-nav-tabbar { 288 | top: 0; 289 | bottom: initial; 290 | border-top: 0; 291 | border-bottom: 1px solid #c7c7c7; 292 | } */ 293 | 294 | 295 | /* .h-nav-tabs-wrap{ 296 | padding-bottom: 0; 297 | padding-top: 48px; 298 | } 299 | .h-nav-tabbar{ 300 | top: 0; 301 | bottom: initial; 302 | border-top: 0; 303 | border-bottom: 1px solid #c7c7c7; 304 | } */ -------------------------------------------------------------------------------- /examples/css/transition-mask.css: -------------------------------------------------------------------------------- 1 | /* .h-nav-behavior-loaded > .h-nav-transition{ 2 | transition: all 1s ease; 3 | } 4 | .h-nav-behavior-loaded > .h-nav-transition > .h-nav-page-enter{ 5 | top: -100%; 6 | } 7 | 8 | .h-nav-behavior-switchtab > .h-nav-transition{ 9 | transition: all 1s ease; 10 | } 11 | .h-nav-behavior-switchtab > .h-nav-transition > .h-nav-page-enter{ 12 | top: -33%; 13 | left: -33%; 14 | } 15 | .h-nav-behavior-switchtab > .h-nav-transition > .h-nav-page-leave-to{ 16 | left: 100%; 17 | top: 100%; 18 | } 19 | 20 | .h-nav-tab-behavior-reduce > .h-nav-transition, 21 | .h-nav-tab-behavior-increase > .h-nav-transition{ 22 | transition: all .3s ease; 23 | } 24 | 25 | .h-nav-tab-behavior-reduce > .h-nav-transition > .h-nav-tab-page-enter, 26 | .h-nav-tab-behavior-increase > .h-nav-transition > .h-nav-tab-page-leave-to{ 27 | left: -100%; 28 | } 29 | 30 | .h-nav-tab-behavior-increase > .h-nav-transition > .h-nav-tab-page-enter, 31 | .h-nav-tab-behavior-reduce > .h-nav-transition > .h-nav-tab-page-leave-to{ 32 | left: 100%; 33 | } 34 | .h-nav-tab-behavior-reduce > .h-nav-transition > .h-nav-tab-page-enter, 35 | .h-nav-tab-behavior-increase > .h-nav-transition > .h-nav-tab-page-enter{ 36 | left: calc(100% * var(--h-nav-tab-distance)); 37 | } */ 38 | 39 | 40 | /* .h-nav-tab-behavior-reduce > .h-nav-transition, 41 | .h-nav-tab-behavior-increase > .h-nav-transition{ 42 | transition: all .3s ease; 43 | } 44 | 45 | .h-nav-tab-behavior-reduce > .h-nav-transition > .h-nav-tab-page-enter, 46 | .h-nav-tab-behavior-increase > .h-nav-transition > .h-nav-tab-page-leave-to{ 47 | left: -100%; 48 | } 49 | 50 | .h-nav-tab-behavior-increase > .h-nav-transition > .h-nav-tab-page-enter, 51 | .h-nav-tab-behavior-reduce > .h-nav-transition > .h-nav-tab-page-leave-to{ 52 | left: 100%; 53 | } 54 | 55 | .h-nav-behavior-push > .h-nav-transition, 56 | .h-nav-behavior-back > .h-nav-transition, 57 | .h-nav-behavior-replace > .h-nav-transition{ 58 | transition: all 3s ease; 59 | } 60 | 61 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-enter, 62 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-leave-to, 63 | .h-nav-behavior-replace > .h-nav-transition > .h-nav-page-enter { 64 | transform: translateX(100%); 65 | } 66 | 67 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-leave-to, 68 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-enter, 69 | .h-nav-behavior-replace > .h-nav-transition > .h-nav-page-leave-to{ 70 | transform: translateX(-33%); 71 | } */ 72 | 73 | /* .h-nav-behavior-back > .h-nav-transition > .h-nav-page-leave-to{ 74 | 75 | } 76 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-leave-to{ 77 | transform: translateX(-100%); 78 | } 79 | 80 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-enter{ 81 | transform: translateX(0); 82 | } */ 83 | /* 84 | h-nav-page-enter-to // .h-nav-behavior-back > .h-nav-transition > .h-nav-page-enter{ 85 | transform: translateX(calc(100% * var(--h-nav-distance))); 86 | } */ -------------------------------------------------------------------------------- /examples/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | <% var opts = htmlWebpackPlugin.options, statics = opts.staticMap; data = opts.data; %> 4 | 5 | 6 | 7 | <%=data.title%> 8 | 9 | 10 |
Loading...
11 | <% for(let i in statics){%> 12 | 13 | <%}%> 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/pages/_inner-show.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/pages/api.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /examples/pages/detail.vue: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /examples/pages/index.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /examples/pages/index_modal.vue: -------------------------------------------------------------------------------- 1 | 12 | 31 | 32 | -------------------------------------------------------------------------------- /examples/pages/list.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | 127 | -------------------------------------------------------------------------------- /examples/pages/list_modal.vue: -------------------------------------------------------------------------------- 1 | 11 | 30 | 31 | -------------------------------------------------------------------------------- /examples/pages/me.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/pages/modal.vue: -------------------------------------------------------------------------------- 1 | 10 | 23 | -------------------------------------------------------------------------------- /examples/pages/nav.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/pages/not-found.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/pages/tra-performance-detail.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/pages/tra-performance.vue: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /examples/root.vue: -------------------------------------------------------------------------------- 1 | 7 | 23 | -------------------------------------------------------------------------------- /examples/tip.vue: -------------------------------------------------------------------------------- 1 | 9 | 44 | -------------------------------------------------------------------------------- /examples_vue3/.npmrc: -------------------------------------------------------------------------------- 1 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 2 | phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ 3 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 4 | registry=https://registry.npm.taobao.org -------------------------------------------------------------------------------- /examples_vue3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples_vue3", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "create": "vue create vue3_project" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@vue/cli": "^4.5.15" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/.npmrc: -------------------------------------------------------------------------------- 1 | sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ 2 | phantomjs_cdnurl=https://npm.taobao.org/mirrors/phantomjs/ 3 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 4 | registry=https://registry.npm.taobao.org -------------------------------------------------------------------------------- /examples_vue3/vue3_project/README.md: -------------------------------------------------------------------------------- 1 | # vue3_project 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3_project", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^3.0.0" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "~4.5.0", 16 | "@vue/cli-plugin-eslint": "~4.5.0", 17 | "@vue/cli-service": "~4.5.0", 18 | "@vue/compiler-sfc": "^3.0.0", 19 | "babel-eslint": "^10.1.0", 20 | "eslint": "^6.7.2", 21 | "eslint-plugin-vue": "^7.0.0" 22 | }, 23 | "eslintConfig": { 24 | "root": true, 25 | "env": { 26 | "node": true 27 | }, 28 | "extends": [ 29 | "plugin:vue/vue3-essential" 30 | ], 31 | "rules": {} 32 | }, 33 | "browserslist": [ 34 | "> 1%", 35 | "last 2 versions", 36 | "not dead" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezedu/history-navigation-vue/b68b1c59abb8dd4419d01cfc16ffc12b1cc9f311/examples_vue3/vue3_project/public/favicon.ico -------------------------------------------------------------------------------- /examples_vue3/vue3_project/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 18 | 19 | 20 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 73 | 74 | 84 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezedu/history-navigation-vue/b68b1c59abb8dd4419d01cfc16ffc12b1cc9f311/examples_vue3/vue3_project/src/assets/logo.png -------------------------------------------------------------------------------- /examples_vue3/vue3_project/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 41 | 42 | 43 | 59 | -------------------------------------------------------------------------------- /examples_vue3/vue3_project/src/main.js: -------------------------------------------------------------------------------- 1 | import * as vue from 'vue'; 2 | import { fitVue3, plugin } from '../../../src/bundle'; 3 | 4 | import '../../../examples/css/docs.scss'; 5 | import '../../../examples/css/transition-mask.css'; 6 | 7 | import config from '../../../examples/config'; 8 | import Root from '../../../examples/root.vue'; 9 | // import App from './App.vue' 10 | fitVue3(vue); 11 | 12 | const app = vue.createApp(Root); 13 | app.use(plugin, config); 14 | 15 | console.log(app); 16 | 17 | app.mount('#app') 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | hello 9 | 10 | 11 |
Loading...
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "history-navigation-vue", 3 | "version": "1.5.0", 4 | "description": "Native-like Navigation for Web apps.", 5 | "keywords": [ 6 | "history", 7 | "navigation", 8 | "vue", 9 | "controller", 10 | "html5", 11 | "multi-page", 12 | "architecture", 13 | "single-page", 14 | "native-like", 15 | "routing", 16 | "router", 17 | "hybrid", 18 | "web app" 19 | ], 20 | "main": "dist/history-navigation-vue.js", 21 | "license": "MIT", 22 | "author": "hezedu", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/hezedu/history-navigation-vue.git" 26 | }, 27 | "scripts": { 28 | "start": "webpack-dev-server --progress --hot --color", 29 | "build": "npm run build_dev && npm run _build_pro && node script/after_webpack_build.js", 30 | "_build_pro": "webpack --node-env=production --color --progress --config webpack.build.config.js", 31 | "build_dev": "webpack --node-env=development --color --progress --config webpack.build.config.js", 32 | "build_ex": "webpack --node-env=production --color --progress --config webpack.config.js", 33 | "ssr_build": "webpack --color --node-env=production --progress --config ./ssr/webpack.config.js", 34 | "preview_docs": "node server/preview_docs.js", 35 | "preview_ex": "node server/preview_ex.js", 36 | "test": "cd test && npm run test_unit", 37 | "test_unit": "mocha test/unit", 38 | "test_e2e": "mocha test/e2e --timeout=5000" 39 | }, 40 | "files": [ 41 | "dist" 42 | ], 43 | "devDependencies": { 44 | "@babel/core": "^7.16.0", 45 | "@babel/eslint-parser": "^7.16.3", 46 | "@babel/plugin-proposal-class-properties": "^7.16.0", 47 | "@babel/plugin-proposal-decorators": "^7.16.4", 48 | "@babel/plugin-proposal-do-expressions": "^7.16.0", 49 | "@babel/plugin-proposal-export-default-from": "^7.16.0", 50 | "@babel/plugin-proposal-export-namespace-from": "^7.16.0", 51 | "@babel/plugin-proposal-function-bind": "^7.16.0", 52 | "@babel/plugin-proposal-function-sent": "^7.16.0", 53 | "@babel/plugin-proposal-json-strings": "^7.16.0", 54 | "@babel/plugin-proposal-logical-assignment-operators": "^7.16.0", 55 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.0", 56 | "@babel/plugin-proposal-numeric-separator": "^7.16.0", 57 | "@babel/plugin-proposal-optional-chaining": "^7.16.0", 58 | "@babel/plugin-proposal-pipeline-operator": "^7.16.0", 59 | "@babel/plugin-proposal-throw-expressions": "^7.16.0", 60 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 61 | "@babel/plugin-syntax-import-meta": "^7.8.3", 62 | "@babel/plugin-transform-runtime": "^7.16.4", 63 | "@babel/preset-env": "^7.16.4", 64 | "@babel/runtime": "^7.16.3", 65 | "acorn": "^8.6.0", 66 | "autoprefixer": "^9.8.8", 67 | "babel-loader": "^8.2.3", 68 | "babel-plugin-transform-remove-console": "^6.9.4", 69 | "chromedriver": "^96.0.0", 70 | "css-loader": "^6.5.1", 71 | "css-minimizer-webpack-plugin": "^3.2.0", 72 | "eslint": "^8.3.0", 73 | "eslint-plugin-vue": "^8.1.1", 74 | "eslint-webpack-plugin": "^3.1.1", 75 | "express": "^4.17.1", 76 | "html-webpack-plugin": "^5.5.0", 77 | "mini-css-extract-plugin": "^2.4.5", 78 | "mocha": "^9.1.3", 79 | "postcss": "^8.4.4", 80 | "postcss-loader": "^6.2.1", 81 | "selenium-webdriver": "^4.1.0", 82 | "style-loader": "^3.3.1", 83 | "terser": "^5.10.0", 84 | "terser-webpack-plugin": "^5.2.5", 85 | "vue": "2.6.11", 86 | "vue-loader": "^15.9.8", 87 | "vue-server-renderer": "2.6.11", 88 | "vue-template-compiler": "2.6.11", 89 | "webpack": "^5.64.4", 90 | "webpack-cli": "^4.9.1", 91 | "webpack-dev-server": "^4.6.0", 92 | "webpack-merge": "^5.8.0", 93 | "webpack-node-externals": "^3.0.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | var autoprefixer = require('autoprefixer'); 2 | module.exports = { 3 | plugins: [ 4 | autoprefixer 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /script/after_webpack_build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const pkg = require('../package.json'); 4 | const zlib = require('zlib'); 5 | 6 | const banner = 7 | `/*! 8 | * ${pkg.name} v${pkg.version} 9 | * (c) ${new Date().getFullYear()} hezedu 10 | * @license ${pkg.license} 11 | */ 12 | ` 13 | const dir = path.join(__dirname, '../dist'); 14 | const names = fs.readdirSync(dir); 15 | const year = (new Date()).getFullYear() 16 | let totalContent = ''; 17 | names.forEach(name => { 18 | let filename = path.join(dir, name); 19 | let content = fs.readFileSync(filename, 'utf-8'); 20 | content = content.replace('__VERSION__', pkg.version); 21 | content = content.replace('__NOW_YEAR__', year); 22 | content = banner + content; 23 | if(name.lastIndexOf('.min') !== -1){ 24 | totalContent = totalContent + content; 25 | } 26 | console.log(name + ' add commit banner ok'); 27 | fs.writeFileSync(filename, content); 28 | }); 29 | 30 | const zipped = zlib.gzipSync(totalContent); 31 | console.log('Pro minimize files total zipped size: ', (zipped.length / 1024).toFixed(2) + 'kb'); 32 | 33 | -------------------------------------------------------------------------------- /script/common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const child_process = require('child_process'); 3 | const del = require('fuckwinfsdel'); 4 | const webpack = require('webpack'); 5 | // const fs = require('fs'); 6 | const webpackConf = require('../webpack.config'); 7 | const clearDir = require('./get_config')().outputPath; 8 | 9 | module.exports = function(){ 10 | // var build_sh = 'webpack --colors'; 11 | console.log('开始清空:',clearDir, '...'); 12 | del(clearDir, function(err){ 13 | if(err){ 14 | console.error(err); 15 | } 16 | console.log('开始build...'); 17 | webpack(webpackConf, function(err, stats){ 18 | if(err || stats.hasErrors()){ 19 | // const json2 = stats.toJson(); 20 | // fs.writeFileSync(path.join(__dirname, 'errout.json'), JSON.stringify(json2, null, '\t')); 21 | return console.log('build 失败', err, stats.toString({ 22 | // copied from `'minimal'` 23 | all: false, 24 | modules: false, 25 | maxModules: 0, 26 | errors: true, 27 | warnings: true, 28 | // our additional options 29 | moduleTrace: false, 30 | errorDetails: false 31 | })); 32 | } 33 | // const json = stats.toJson(); 34 | // fs.writeFileSync(path.join(__dirname, 'out.json'), JSON.stringify(json, null, '\t')); 35 | // console.log('json', Object.keys(json)); 36 | console.log(stats.toString({ 37 | // Add console colors 38 | colors: true, 39 | chunks: false, 40 | modules: false, 41 | children: false 42 | })); 43 | console.log('build 成功'); 44 | 45 | }); 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /script/get-static-map.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const cdnRootUrl = '/_NODE_MODULE_/'; 4 | module.exports = function(isPro){ 5 | const staticMap = Object.create(null); 6 | ['vue'].forEach(name => { 7 | const mPath = require.resolve(name); 8 | const dir = path.dirname(path.dirname(mPath)); 9 | let pkg = fs.readFileSync(path.join(dir, 'package.json')); 10 | pkg = JSON.parse(pkg); 11 | const version = pkg.version; 12 | pkg = null; 13 | let fileName = name; 14 | if(name === 'vue'){ 15 | fileName = 'vue.runtime'; 16 | } 17 | if(isPro){ 18 | fileName = fileName + '.min.js'; 19 | } else { 20 | fileName = fileName + '.js'; 21 | } 22 | 23 | staticMap[name] = { 24 | version, 25 | fileName, 26 | filePath: path.join(dir, 'dist/' + fileName), 27 | // url: '/static/' + name + '@' + version + '/dist/' + fileName 28 | url: cdnRootUrl + name + '@' + version + '/dist/' + fileName 29 | } 30 | }); 31 | 32 | return staticMap; 33 | } 34 | -------------------------------------------------------------------------------- /script/get_config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | function get(){ 4 | const OUT_FOLDER = 'built'; 5 | const isPro = process.env.NODE_ENV === 'production'; 6 | 7 | let baseUrl = ''; 8 | let indexDir; 9 | let bundleName; 10 | let publicPath; 11 | let outputPath; 12 | 13 | if(isPro){ 14 | indexDir = path.join(__dirname, '../dist'); 15 | bundleName = '[name]_[contenthash].min.js'; 16 | publicPath = OUT_FOLDER; 17 | outputPath = path.join(indexDir, publicPath + '/'); 18 | } else { 19 | indexDir = path.join(__dirname, '../'); 20 | bundleName = '[name].js'; 21 | publicPath = 'dev_dist/' + OUT_FOLDER; 22 | outputPath = path.join(indexDir, publicPath + '/'); 23 | } 24 | 25 | publicPath = baseUrl + '/' + publicPath + '/'; 26 | 27 | // if(indexDir[indexDir.length - 1] === path.sep){ 28 | // indexDir = indexDir.substr(0, indexDir.length - 1); 29 | // } 30 | return { 31 | indexDir, 32 | bundleName, 33 | outputPath, 34 | publicPath 35 | } 36 | } 37 | 38 | 39 | module.exports = get; -------------------------------------------------------------------------------- /script/pro.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production'; 2 | var bulid = require('./common'); 3 | bulid(); 4 | -------------------------------------------------------------------------------- /server/preview.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const app = express(); 4 | const config = require('../config'); 5 | // var proxy = require('http-proxy-middleware'); 6 | // app.use(express.static('/', path.join(__dirname, '../dist'))); 7 | // app.use('/api', proxy({target: '', changeOrigin: true})); 8 | app.use('/', express.static(path.join(__dirname, '../dist'))); 9 | const port = config.previewPort; 10 | app.listen(port, () => console.log(`Preview app listening on port ${port}!`)) -------------------------------------------------------------------------------- /server/preview_docs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const config = require('../config'); 4 | 5 | const app = express(); 6 | app.use('/history-navigation-vue/', express.static(path.join(__dirname, '../docs'))); 7 | const port = config.previewDocsPort; 8 | app.listen(port, () => console.log(`Preview app listening on port ${port}!`)) -------------------------------------------------------------------------------- /server/preview_ex.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const config = require('../config'); 4 | const setup = require('./setup'); 5 | const app = express(); 6 | setup(app); 7 | 8 | // dev 9 | app.use('/dev_dist', express.static(path.join(__dirname, '../dev_dist'))); 10 | app.use('/dev_favicon.png', express.static(path.join(__dirname, '../dev_favicon.png'))); 11 | const port = config.previewExPort; 12 | app.listen(port, () => console.log(`Preview app listening on port ${port}!`)) -------------------------------------------------------------------------------- /server/setup.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const eStatic = require('express').static; 4 | const getStaticMap = require('../script/get-static-map'); 5 | const staticMap = getStaticMap(); 6 | 7 | function setup(app){ 8 | app.get('/favicon.ico', (req, res) => { 9 | res.set({ 10 | 'Cache-Control': 'public, max-age=91104000' 11 | }); 12 | res.status(410).end('Gone'); 13 | }); 14 | 15 | let v; 16 | for(let i in staticMap){ 17 | v = staticMap[i]; 18 | app.use(v.url, eStatic(v.filePath)); 19 | } 20 | 21 | app.get('*', (req, res, next) => { 22 | if(req.url.indexOf('/static') === 0 || 23 | req.url.indexOf('/dev') === 0){ 24 | next(); 25 | } else { 26 | res.type('html').send( fs.readFileSync(path.join(__dirname, '../index.html'))); 27 | } 28 | }); 29 | 30 | 31 | 32 | 33 | 34 | }; 35 | 36 | setup.staticMap = staticMap; 37 | module.exports = setup; -------------------------------------------------------------------------------- /src/bundle.js: -------------------------------------------------------------------------------- 1 | import './css/style.css'; 2 | // import './css/transition.css'; 3 | import install from './install'; 4 | 5 | export const plugin = { 6 | install 7 | } 8 | 9 | // export { fitVue$3 } from './fit_vue'; 10 | 11 | export const version = '__VERSION__'; 12 | -------------------------------------------------------------------------------- /src/cmpt/common.js: -------------------------------------------------------------------------------- 1 | export function genPageProps(){ 2 | return { 3 | path: { 4 | type: String 5 | }, 6 | 7 | title: String, 8 | 9 | isTab: { 10 | type: Boolean, 11 | required: true 12 | }, 13 | 14 | tabIndex: Number, 15 | 16 | stateKey: Number, 17 | 18 | route: { 19 | type: Object, 20 | required: true 21 | }, 22 | 23 | isActive: { 24 | type: Boolean, 25 | required: true 26 | }, 27 | 28 | isFirstLoad: { 29 | type: Boolean, 30 | required: true 31 | }, 32 | 33 | stackId: Number 34 | } 35 | } -------------------------------------------------------------------------------- /src/cmpt/default-not-found.vue: -------------------------------------------------------------------------------- 1 | 2 | 14 | 23 | -------------------------------------------------------------------------------- /src/cmpt/navigation-controller.vue: -------------------------------------------------------------------------------- 1 | 2 | 52 | 107 | -------------------------------------------------------------------------------- /src/cmpt/navigator.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/cmpt/page.vue: -------------------------------------------------------------------------------- 1 | 33 | -------------------------------------------------------------------------------- /src/cmpt/page_main.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/cmpt/tab-bar-controller.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /src/cmpt/tab-bar.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 72 | -------------------------------------------------------------------------------- /src/constant.js: -------------------------------------------------------------------------------- 1 | export const PAGE_E_SHOW_NAME = '_h_nav_page_show'; 2 | export const PAGE_E_HIDE_NAME = '_h_nav_page_hide'; 3 | 4 | export const cmptPageSuffix = 'h-nav-page-'; 5 | export const notFoundPageKey = cmptPageSuffix + 'not-found'; 6 | 7 | // export const DEF_IS_SET_HREF = true; 8 | export const DEF_URL_IS_HASH_MODE = true; 9 | export const DEF_NAVIGATOR_TRIGGER_EVENT = 'click'; 10 | export const DEF_URL_BASE = ''; 11 | 12 | export const DEF_TRANSITION = 'h-nav-transition'; 13 | 14 | export const KEY_NAME = '_h_n_key'; 15 | export const MODAL_CRUMBS_KEY_NAME = '_h_n_crumbs'; 16 | export const MODAL_KEY_NAME = '_h_n_modal_key'; 17 | export const BACK_TRA_PROP_KEY = '_h_n_b_tra'; 18 | 19 | export const MODAL_BAE_KEY = '_H_NAV_BAE_MODAL_'; 20 | 21 | export const DEF_PAGE_STYLE = undefined; 22 | 23 | // export const AGAIN_TO_EXIT_INTERVAL = 2000; 24 | // export const AGAIN_TO_EXIT_TIP = 'Press Back Again to Exit'; -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | .h-nav-ctrler{ 2 | width: 100%; 3 | height: 100%; 4 | overflow: hidden; 5 | position: relative; 6 | } 7 | 8 | .h-nav-page, .h-nav-tab-page { 9 | position: absolute; 10 | left: 0; 11 | top: 0; 12 | height: 100%; 13 | width: 100%; 14 | } 15 | 16 | .h-nav-page-main { 17 | background-color: #fff; 18 | position: relative; 19 | -webkit-overflow-scrolling: touch; 20 | overflow: auto; 21 | width: 100%; 22 | height: 100%; 23 | } 24 | 25 | .h-nav-tabs-ctrler{ 26 | position: absolute; 27 | left: 0; 28 | top: 0; 29 | bottom: 0; 30 | right: 0; 31 | overflow: hidden; 32 | } 33 | 34 | 35 | 36 | .h-nav-page-handle, 37 | .h-nav-tab-page-handle{ 38 | width: 0!important; 39 | height: 0!important; 40 | border: 0!important; 41 | margin: 0!important; 42 | padding: 0!important; 43 | box-shadow: none!important; 44 | outline: none!important; 45 | position: static!important; 46 | } 47 | 48 | .h-nav-page-enter-active, 49 | .h-nav-page-leave-active, 50 | .h-nav-tab-page-enter-active, 51 | .h-nav-tab-page-leave-active{ 52 | transition: inherit; 53 | animation: inherit; 54 | } 55 | 56 | .h-nav-tab-pages-wrap { 57 | transition: inherit; 58 | animation: inherit; 59 | position: static!important; 60 | transition-property: none; 61 | animation-name: none; 62 | } 63 | 64 | .h-nav-tab-page-handle{ 65 | transition: inherit; 66 | animation: inherit; 67 | } 68 | .h-nav-tab-behavior-_inherit{ 69 | transition: inherit!important; 70 | animation: inherit!important; 71 | } 72 | 73 | .h-nav-modal { 74 | position: absolute; 75 | top: 0; 76 | left: 0; 77 | width: 100%; 78 | height: 100%; 79 | color: #fff; 80 | box-sizing: border-box; 81 | /* background-color: rgba(0, 0, 0, .5); */ 82 | } 83 | 84 | /* .h-nav-tab-page .h-nav-modal { 85 | z-index: 2000; 86 | } */ 87 | .h-nav-page-has-modal { 88 | overflow: hidden; 89 | } 90 | 91 | .h-nav-behavior-push > .h-nav-transition, 92 | .h-nav-behavior-back > .h-nav-transition, 93 | .h-nav-behavior-replace > .h-nav-transition{ 94 | transition: all .3s ease; 95 | } 96 | 97 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-enter, 98 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-leave-to { 99 | transform: translateX(100%); 100 | } 101 | 102 | .h-nav-behavior-replace > .h-nav-transition > .h-nav-page-enter { 103 | transform: translateY(100%); 104 | } 105 | 106 | /* ---------------- behavior push/back start ---------------- */ 107 | /* .h-nav-behavior-push > .h-nav-transition, 108 | .h-nav-behavior-back > .h-nav-transition, 109 | .h-nav-behavior-replace > .h-nav-transition{ 110 | transition: all .3s ease; 111 | } 112 | 113 | .h-nav-behavior-push > .h-nav-transition > .h-nav-page-enter, 114 | .h-nav-behavior-back > .h-nav-transition > .h-nav-page-leave-to, 115 | .h-nav-behavior-replace > .h-nav-transition > .h-nav-page-enter { 116 | transform: translateX(100%); 117 | } */ 118 | /* ---------------- behavior push/back end ---------------- */ -------------------------------------------------------------------------------- /src/fit_vue.js: -------------------------------------------------------------------------------- 1 | 2 | export function uniteVue2(_Vue){ 3 | 4 | return { 5 | proto: _Vue.prototype, 6 | nextTick: _Vue.nextTick, 7 | newComponent: function(Cmpt, newOpts) { 8 | const CmptExtend = _Vue.extend(Cmpt); 9 | return new CmptExtend(newOpts); 10 | }, 11 | destroy: function(cmpt){ 12 | cmpt.$destroy(); 13 | }, 14 | set: _Vue.set, 15 | delete: _Vue.delete 16 | } 17 | } 18 | 19 | /* FIT_VUE_3_SWITCH 20 | let _Vue3; // createApp, h, nextTick; 21 | export function fitVue3(_Vue){ 22 | _Vue3 = _Vue; 23 | } 24 | 25 | export function uniteVue3(app){ 26 | return { 27 | proto: app.config.globalProperties, 28 | nextTick: _Vue3.nextTick, 29 | newComponent: (Cmpt, newOpts) => { 30 | let app = _Vue3.createApp(Cmpt, newOpts.propsData); 31 | app.mount(newOpts.el); 32 | return app; 33 | }, 34 | destroy: function(cmpt){ 35 | cmpt.unmount(); 36 | }, 37 | set: function(obj, k, v){ 38 | obj[k] = v; 39 | }, 40 | delete: function(obj, k){ 41 | delete obj[k]; 42 | } 43 | } 44 | } 45 | 46 | export function vue3Reactive(obj){ // vue3 API 47 | return _Vue3.reactive(obj); 48 | } 49 | 50 | export function vue3FitEmitOnOff(self){ 51 | const eMap = self.$options._tmp_e_map = Object.create(null); 52 | self.$on = function(key, handle){ 53 | let list = eMap[key]; 54 | if(!list){ 55 | list = eMap[key] = []; 56 | } 57 | list.push(handle); 58 | } 59 | 60 | self.$off = function(key, handle){ 61 | const list = eMap[key]; 62 | if(list){ 63 | const i = list.indexOf(handle); 64 | if(i !== -1){ 65 | list.splice(i, 1); 66 | } 67 | } 68 | if(!list.length){ 69 | delete eMap[key]; 70 | } 71 | } 72 | 73 | self._uniteEmit = function(key) { 74 | const list = eMap[key]; 75 | if(list) { 76 | const len = list.length; 77 | let i = 0; 78 | for(; i < len; i++){ 79 | list[i](); 80 | } 81 | } 82 | } 83 | } 84 | 85 | */ -------------------------------------------------------------------------------- /src/h_nav/h_nav.js: -------------------------------------------------------------------------------- 1 | 2 | function H_NAV(opt){ 3 | this._opt = Object.assign(Object.create(null), opt); 4 | 5 | if(!this._opt.native){ 6 | if(typeof window === undefined){ 7 | throw new Error(_wrap_error('window is undefined')); 8 | } 9 | this._navite = { 10 | window: window, 11 | history: window.history, 12 | location: window.location 13 | } 14 | } else { 15 | this._navite = Object.assign(Object.create(null), this._opt.native); 16 | delete this._opt.native; 17 | } 18 | this.init(); 19 | } 20 | 21 | const PAGE_KEY = '_h_nav_page'; 22 | 23 | H_NAV.prototype.init = function(){ 24 | const state = this._navite.history.state || {}; 25 | if(!state[PAGE_KEY]){ 26 | 27 | } 28 | } 29 | 30 | function _wrap_error(msg){ 31 | return 'Error: h_nav ' + msg; 32 | } -------------------------------------------------------------------------------- /src/install.js: -------------------------------------------------------------------------------- 1 | import NavigationController from './cmpt/navigation-controller.vue'; 2 | import DefaultNotFound from './cmpt/default-not-found.vue'; 3 | import Navigator from './cmpt/navigator.vue'; 4 | import navigator from './navigator/navigator'; 5 | import { trimSlash } from './navigator/url'; 6 | import ShowHideMixin from './mixin/show-hide-mixin'; 7 | import { def, throwErr, getVueV, noop } from './util'; 8 | import { uniteVue2} from './fit_vue'; 9 | import {cmptPageSuffix, 10 | notFoundPageKey, 11 | DEF_NAVIGATOR_TRIGGER_EVENT, 12 | DEF_URL_BASE, 13 | DEF_URL_IS_HASH_MODE, 14 | DEF_TRANSITION, 15 | DEF_PAGE_STYLE } from './constant'; 16 | 17 | 18 | const defNotFoundPage = { 19 | title: 'Not Found', 20 | component: DefaultNotFound 21 | } 22 | 23 | export default function install(_Vue, config) { 24 | const vueV = getVueV(_Vue); 25 | let uniteVue; 26 | if(vueV === 2){ 27 | uniteVue = uniteVue2(_Vue); 28 | // } else if(vueV === 3){ // FIT_VUE_3_SWITCH 29 | // uniteVue = uniteVue$3(_Vue); 30 | // uniteVue.is3 = true; 31 | } else { 32 | throwErr('Unsupported version of Vue ' + vueV); 33 | } 34 | 35 | 36 | if(!Array.isArray(config.pages)){ 37 | throwErr('config.pages is not Array.'); 38 | } 39 | 40 | const pageMap = _formatPages(config.pages); 41 | let tabBar; 42 | if(config.tabBar){ 43 | tabBar = _formatTabBar(config.tabBar, pageMap); 44 | } 45 | 46 | let notFoundPage = config.notFoundPage || defNotFoundPage; 47 | _Vue.component(notFoundPageKey, notFoundPage.component); 48 | 49 | notFoundPage = { 50 | path: null, 51 | title: notFoundPage.title, 52 | cmptKey: notFoundPageKey, 53 | isTab: false, 54 | 55 | trimedPath: null 56 | } 57 | 58 | let i, page; 59 | for(i in pageMap){ 60 | page = pageMap[i]; 61 | _Vue.component(page.cmptKey, page.component); 62 | } 63 | 64 | _Vue.component('NavigationController', NavigationController); 65 | _Vue.component('Navigator', Navigator); 66 | 67 | const globalOption = Object.create(null); 68 | def(globalOption, config, 'navigatorTriggerEvent', DEF_NAVIGATOR_TRIGGER_EVENT); 69 | def(globalOption, config, 'transition', DEF_TRANSITION); 70 | def(globalOption, config, 'pageStyle', DEF_PAGE_STYLE); 71 | def(globalOption, config, 'homePagePath', config.pages[0].path); 72 | const homePage = pageMap[trimSlash(globalOption.homePagePath)]; 73 | 74 | if(!homePage){ 75 | throwErr('Home page not found'); 76 | } 77 | 78 | homePage.isHome = true; 79 | 80 | let BAE = null; 81 | if(config.backAgainToExit){ 82 | BAE = Object.create(null); 83 | def(BAE, config.backAgainToExit, 'maxInterval', 2000); 84 | def(BAE, config.backAgainToExit, 'onFirstTrigger', noop); 85 | } 86 | 87 | const options = { 88 | global: globalOption, 89 | BAE, 90 | uniteVue, 91 | pageMap, 92 | cmptPageSuffix, 93 | notFoundPage, 94 | tabBar, 95 | onRouted: config.onRouted 96 | } 97 | 98 | def(options, config, 'urlBase', DEF_URL_BASE); 99 | def(options, config, 'urlIsHashMode', DEF_URL_IS_HASH_MODE); 100 | 101 | uniteVue.proto.$navigator = navigator(options); 102 | _Vue.mixin(ShowHideMixin(uniteVue.is3)); 103 | } 104 | 105 | function _formatPages(pages){ 106 | 107 | let map = Object.create(null); 108 | let i = 0, len = pages.length, page, tk, fpage; 109 | for(; i < len; i++){ 110 | page = pages[i]; 111 | tk = trimSlash(page.path); 112 | if(map[tk]){ 113 | throwErr(`pageMap key: ${tk} is same as ${page.path}`); 114 | } 115 | fpage = Object.assign({}, page); 116 | 117 | Object.assign(fpage, { 118 | trimedPath: tk, 119 | isTab: false, 120 | isHome: false, 121 | cmptKey: cmptPageSuffix + i 122 | }); 123 | 124 | map[tk] = fpage; 125 | } 126 | return map; 127 | } 128 | 129 | 130 | function _formatTabBar(tabBar, pageMap){ 131 | const list = tabBar.list; 132 | let len = list.length; 133 | if(len < 2){ 134 | throwErr(`tabBar list length must >= 2`); 135 | } 136 | let i = 0, tk, item; 137 | 138 | const map = Object.create(null); 139 | const newList = []; 140 | for(; i < len; i++){ 141 | item = list[i]; 142 | tk = trimSlash(item.pagePath); 143 | const page = pageMap[tk]; 144 | if(!page || page.path !== item.pagePath){ 145 | throwErr(`tabBar pagePath: ${i} is not found in pages`); 146 | } 147 | if(map[tk]){ 148 | throwErr(`tabBar pagePath: ${tk} is same as ${i}`); 149 | } 150 | page.isTab = true; 151 | page.tabIndex = i; 152 | map[tk] = true; 153 | newList.push(Object.assign({}, item)); 154 | } 155 | return { 156 | map, 157 | list: newList 158 | }; 159 | } 160 | 161 | 162 | -------------------------------------------------------------------------------- /src/mixin/show-hide-mixin.js: -------------------------------------------------------------------------------- 1 | import { PAGE_E_SHOW_NAME, PAGE_E_HIDE_NAME } from '../constant'; 2 | export default function (isV3) { 3 | const key = isV3 ? 'unmounted' : 'destroyed'; 4 | return { 5 | inject: { 6 | $page: { 7 | default: null 8 | } 9 | }, 10 | created(){ 11 | if((this.$options.onShow || this.$options.onHide) && this.$page){ 12 | _handleOnShow(this); 13 | } 14 | }, 15 | [key](){ 16 | if(this.$options.__tmp_h_nav_show_hide_has){ 17 | _handleOnHide(this); 18 | } 19 | } 20 | } 21 | } 22 | 23 | function _handleOnShow(self){ 24 | const $opts = self.$options; 25 | $opts.__tmp_h_nav_show_hide_has = true; 26 | let handle; 27 | if($opts.onShow){ 28 | handle = $opts.__tmp_h_nav_page_show_hanlde = () => { 29 | $opts.onShow.call(self); 30 | } 31 | self.$page.$on(PAGE_E_SHOW_NAME, handle); 32 | } 33 | if($opts.onHide){ 34 | handle = $opts.__tmp_h_nav_page_hide_hanlde = () => { 35 | $opts.onHide.call(self); 36 | } 37 | self.$page.$on(PAGE_E_HIDE_NAME, handle); 38 | } 39 | } 40 | 41 | function _handleOnHide(self){ 42 | const $opts = self.$options; 43 | let handle = $opts.__tmp_h_nav_page_show_hanlde; 44 | if(handle){ 45 | self.$page.$off(PAGE_E_SHOW_NAME, handle); 46 | } 47 | handle = $opts.__tmp_h_nav_page_hide_hanlde; 48 | if(handle){ 49 | self.$page.$off(PAGE_E_HIDE_NAME, handle); 50 | } 51 | } -------------------------------------------------------------------------------- /src/navigator/history.js: -------------------------------------------------------------------------------- 1 | import { nativeWindow, nativeHistory, nativeLocation } from './native'; 2 | import URL, { fullUrlParse } from './url'; 3 | import { getCurrentStateKey, genStateKey, getCurrModaKey, getPreState, updatePreState, getCurrState } from './state-key'; 4 | import { KEY_NAME, BACK_TRA_PROP_KEY } from '../constant'; 5 | import { noop, throwErr } from '../util'; 6 | import modalPart from './libs/modal'; 7 | import BAEPart from './libs/bae'; 8 | import BackPart from './libs/back'; 9 | let isCreated = false; 10 | 11 | function History(opt){ 12 | if(isCreated){ 13 | throwErr('Only one instance can be generated.'); 14 | } 15 | isCreated = true; 16 | this._global = opt.global; 17 | this._window = nativeWindow; 18 | this._history = nativeHistory; 19 | this._location = nativeLocation; 20 | this._isOmitForwardEvent = false; 21 | this.BAE = opt.BAE; 22 | // this._exitImmediately = true; 23 | // this.onExit = opt.onExit; // Chrome must touch the document once to work. 24 | this._tra = {className: this._global.transition}; 25 | this.uniteVue = opt.uniteVue; 26 | this.pageMap = opt.pageMap; 27 | this.notFoundPage = opt.notFoundPage; 28 | if(opt.tabBar){ 29 | this.tabMap = opt.tabBar.map; 30 | this.tabList = opt.tabBar.list; 31 | this.tabStackMap = Object.create(null); 32 | } 33 | this.stackMap = Object.create(null); 34 | 35 | this._stackItemId = 1; 36 | this.tabCtrlerStackId = 1; 37 | // this.isPageDestroyWhenBack = true; 38 | this.onRouted = opt.onRouted || noop; 39 | 40 | this.URL = new URL({isHashMode: opt.urlIsHashMode, base: opt.urlBase}); 41 | 42 | this.behavior = { 43 | type: '', 44 | distance: 0, 45 | isPop: false 46 | } 47 | 48 | this.currentPage = { 49 | path: null, 50 | title: null, 51 | className: undefined, 52 | 53 | isHome: false, 54 | isTab: false, 55 | tabIndex: null, 56 | cmptKey: null, 57 | 58 | stackId: null, 59 | 60 | stateKey: null, 61 | modalList: [], 62 | isClean: false, 63 | route: {} 64 | } 65 | BackPart.init.apply(this); 66 | modalPart.init.apply(this); 67 | // this.fitVue$3(); // FIT_VUE_3_SWITCH 68 | } 69 | 70 | History.prototype.checkCompatibility = function(){ 71 | if(this._history && this._history.pushState){ 72 | return true; 73 | } 74 | return false; 75 | } 76 | 77 | History.prototype._bind = function(){ 78 | this._popstateHandle = () => { 79 | this.handlePop(); 80 | } 81 | this._window.addEventListener('popstate', this._popstateHandle); 82 | 83 | this._handleWinUnload = () => { 84 | this.handleWinUnload(); 85 | } 86 | this._window.addEventListener('beforeunload', this._handleWinUnload); 87 | } 88 | 89 | History.prototype._onRouted = function(){ 90 | const curr = this.currentPage; 91 | this.onRouted({ 92 | title: curr.title, 93 | routeFullPath: curr.route.fullPath 94 | }); 95 | this._autoBAE(curr.route.trimedPath); 96 | } 97 | 98 | History.prototype._genStackItemId = function(){ 99 | this._stackItemId = this._stackItemId + 1; 100 | return 'stack_' + this._stackItemId; 101 | } 102 | 103 | History.prototype._isTabRoute = function(trimedPath){ 104 | if(!this.tabMap){ 105 | return false; 106 | } 107 | if(this.tabMap[trimedPath]){ 108 | return true; 109 | } 110 | return false; 111 | } 112 | 113 | 114 | History.prototype._forMatInputArg = function(opt){ 115 | let fullParse, tra; 116 | if(typeof opt === 'string'){ 117 | fullParse = fullUrlParse(opt); 118 | } else { 119 | fullParse = fullUrlParse({ 120 | path: opt.url, 121 | query: opt.query 122 | }) 123 | tra = opt.transition; 124 | } 125 | return { 126 | fullParse, 127 | tra 128 | }; 129 | } 130 | 131 | 132 | History.prototype._load = function(userUrl){ 133 | console.log('load', this.BAE, this.pageMap); 134 | this._bind(); 135 | const _userUrl = userUrl === undefined ? 136 | this.URL.getUrlByLocation() : 137 | userUrl; 138 | const currRoute = fullUrlParse(_userUrl); 139 | const key = getCurrentStateKey(); 140 | if(key !== 1){ 141 | if(this._isTabRoute(currRoute.trimedPath)){ 142 | this._backToStartAndReplace(currRoute, 'loaded'); 143 | return; 144 | } 145 | } 146 | this._replaceCurrPage(currRoute, 'loaded'); 147 | } 148 | 149 | History.prototype._replaceCurrPage = function(){ 150 | const modalCount = getCurrModaKey(); 151 | if(modalCount){ 152 | this._backAndApply(modalCount, '_replace', arguments); 153 | return; 154 | } 155 | this._replace.apply(this, arguments); 156 | } 157 | 158 | History.prototype._setTra = function(className){ 159 | this._tra.className = className || this._global.transition; 160 | } 161 | 162 | History.prototype.push = function(userUrl){ 163 | const { fullParse, tra } = this._forMatInputArg(userUrl); 164 | if(this._isTabRoute(fullParse.trimedPath)){ 165 | console.error('Cannot push the tab url, please use switchTab'); 166 | return; 167 | } 168 | this._setTra(tra); 169 | this._push(fullParse, tra); 170 | } 171 | 172 | History.prototype.replace = function(userUrl){ 173 | const { fullParse, tra } = this._forMatInputArg(userUrl); 174 | if(this._isTabRoute(fullParse.trimedPath)){ 175 | console.error('Cannot replace the tab url, please use switchTab'); 176 | return; 177 | } 178 | this._setTra(tra); 179 | this._replaceCurrPage(fullParse); 180 | } 181 | 182 | 183 | 184 | History.prototype.switchTab = function(userUrl){ 185 | // if(userUrl.indexOf('?') !== -1){ 186 | // console.error('switchTab not support queryString'); 187 | // } 188 | const { fullParse, tra } = this._forMatInputArg(userUrl); 189 | if(!this._isTabRoute(fullParse.trimedPath)){ 190 | console.error(fullParse, ' is not tab url'); 191 | return; 192 | } 193 | 194 | this._backToStartAndReplace(fullParse, 'switchtab', tra); 195 | } 196 | 197 | History.prototype.relaunch = function(userUrl){ 198 | const { fullParse, tra } = this._forMatInputArg(userUrl); 199 | this.tabCtrlerStackId = this.tabCtrlerStackId + 1; 200 | this._backToStartAndReplace(fullParse, 'relaunch', tra); 201 | } 202 | 203 | History.prototype._setMapItem = function(key, route){ 204 | 205 | 206 | let page = this.pageMap[route.trimedPath] || this.notFoundPage; 207 | const _page = { 208 | 209 | path: page.path, 210 | title: page.title, 211 | tabIndex: page.tabIndex, 212 | route, 213 | cmptKey: page.cmptKey, 214 | isHome: page.isHome, 215 | isTab: page.isTab, 216 | stateKey: key, 217 | className: page.className, 218 | style: page.style, 219 | modalList: [], 220 | isClean: false // when curr page leaveing, It doesn't work. 221 | } 222 | 223 | if(_page.isTab){ 224 | _page.stackId = 'tab_stack_' + this.tabCtrlerStackId; 225 | this.uniteVue.set(this.tabStackMap, page.tabIndex, _page); 226 | } else { 227 | _page.stackId = this._genStackItemId(); 228 | } 229 | 230 | 231 | 232 | 233 | // Object.assign(this.currentPage, _page); 234 | this.uniteVue.set(this.stackMap, key, _page); 235 | Object.assign(this.currentPage, _page); 236 | } 237 | History.prototype._getBackTra = function(){ 238 | const H = this._history; 239 | if(H.state){ 240 | return H.state[BACK_TRA_PROP_KEY]; 241 | } 242 | } 243 | 244 | History.prototype._push = function(fullParse, tra){ 245 | this._autoBAE(); 246 | /* 247 | from [vue-router] 248 | try...catch the pushState call to get around Safari 249 | DOM Exception 18 where it limits to 100 pushState calls 250 | */ 251 | // this._clearAf2ter(); 252 | const oldTra = this._getBackTra(); 253 | 254 | if(tra !== oldTra){ 255 | let state = Object.assign({}, this._history.state); 256 | if(tra){ 257 | state[BACK_TRA_PROP_KEY] = tra; 258 | } else { 259 | delete state[BACK_TRA_PROP_KEY]; 260 | } 261 | 262 | this._history.replaceState(state, ''); 263 | } 264 | const key = genStateKey(); 265 | 266 | 267 | this._setModalCrumbsWhenChange(); 268 | this._history.pushState({[KEY_NAME]: key}, '', this.URL.toLocationUrl(fullParse.fullPath)); 269 | 270 | updatePreState(); 271 | 272 | const newBehavior = { 273 | type: 'push', 274 | distance: 1, 275 | isPop: false 276 | } 277 | Object.assign(this.behavior, newBehavior); 278 | 279 | this._setMapItem(key, fullParse); 280 | 281 | this._onRouted(); 282 | } 283 | 284 | // History.prototype._replaceCurrPage = function(fullParse, behavior, _dista2nce){ 285 | // const isBAE = this._isBAEPage(); 286 | // const isDistBAE = this._isBAEPageByTK(fullUrlParse); 287 | // let step = this.get2CurrModaKey(); 288 | // if(isBAE && !isDistBAE){ 289 | // step = step + 1; 290 | // } 291 | // if(step){ 292 | // this._backAndApply(step, '_repl2ace', arguments); 293 | // return; 294 | // } 295 | // this._repl2ace.apply(this, arguments); 296 | // } 297 | 298 | 299 | History.prototype._replace = function(fullParse, behavior){ 300 | const preKey = getPreState().key; 301 | const key = getCurrentStateKey(); 302 | const distance = key - preKey; 303 | const newBehavior = { 304 | type: behavior || 'replace', 305 | distance, 306 | isPop: false 307 | } 308 | Object.assign(this.behavior, newBehavior); 309 | if(this.behavior.type !== 'switchtab' || distance){ 310 | // unactive currentPage 311 | this.currentPage.stackId = 'unactive_' + this.currentPage.stackId; 312 | } 313 | const toUrl = this.URL.toLocationUrl(fullParse.fullPath); 314 | let state = this._history.state; 315 | if(!state || !state[KEY_NAME]){ 316 | state = Object.assign({}, history.state); 317 | state[KEY_NAME] = key; 318 | } 319 | 320 | this._history.replaceState(state, '', toUrl); 321 | updatePreState(); 322 | 323 | this.uniteVue.nextTick(() => { 324 | if(newBehavior.type === 'relaunch'){ 325 | this._setAllCleaned(); 326 | const oldKey = key - distance; 327 | this.stackMap[oldKey].isClean = false; 328 | this.uniteVue.nextTick(() => { 329 | this._clearAll(); 330 | this._setMapItem(key, fullParse); 331 | this._onRouted(); 332 | }) 333 | 334 | } else { 335 | this._clearAfter(); 336 | this._setMapItem(key, fullParse); 337 | this._onRouted(); 338 | } 339 | }); 340 | } 341 | 342 | 343 | 344 | 345 | 346 | History.prototype._setMapCleaned = function(map){ 347 | for(let i in map){ 348 | map[i].isClean = true; 349 | } 350 | } 351 | History.prototype._setAllCleaned = function(){ 352 | if(this.tabStackMap){ 353 | this._setMapCleaned(this.tabStackMap); 354 | } 355 | this._setMapCleaned(this.stackMap); 356 | } 357 | 358 | 359 | 360 | // History.prototype._backToStartAndRe2place = function(fullParse, behavior, tra){ 361 | // const key = getCurrentStateKey(); 362 | // if(key > 1){ 363 | // this._whenPopInfo = { 364 | // fullParse, 365 | // behavior 366 | // }; 367 | // this.back(key - 1, tra); 368 | // } else { 369 | // this._setTra(tra); 370 | // this._rep2lace(fullParse, behavior); 371 | // } 372 | // } 373 | 374 | History.prototype._clearAfter = function(){ 375 | const key = getCurrentStateKey(); 376 | const map = this.stackMap; 377 | let i, v; 378 | const arr = []; 379 | for (i in map) { 380 | v = map[i]; 381 | if (v.stateKey > key) { 382 | v.isClean = true; 383 | arr.push(v); 384 | } 385 | } 386 | let len = arr.length; 387 | if(len){ 388 | const last = arr.pop(); 389 | last.isClean = false; 390 | this.uniteVue.delete(map, last.stateKey); 391 | len = arr.length; 392 | if(len){ 393 | this.uniteVue.nextTick(() => { 394 | i = 0; 395 | for(; i < len; i++){ 396 | v = arr[i]; 397 | this.uniteVue.delete(map, v.stateKey); 398 | } 399 | }) 400 | } 401 | } 402 | } 403 | 404 | History.prototype._clearMap = function(map){ 405 | let i; 406 | for(i in map){ 407 | this.uniteVue.delete(map, i); 408 | } 409 | } 410 | 411 | History.prototype._clearAll = function(){ 412 | if(this.tabStackMap){ 413 | this._clearMap(this.tabStackMap); 414 | } 415 | this._clearMap(this.stackMap); 416 | } 417 | 418 | 419 | History.prototype.handlePop = function(){ 420 | if(this._isOmitForwardEvent){ 421 | this._isOmitForwardEvent = false; 422 | return; 423 | } 424 | console.log('[handlePop]'); 425 | let _backInfo = this._whenBackPopInfo; 426 | if(_backInfo){ 427 | this[_backInfo.method].apply(this, _backInfo.args); 428 | // this._set2ModalCrumbsWhenChange(); 429 | this._whenBackPopInfo = null; 430 | return; 431 | } 432 | 433 | const preState = getPreState(); 434 | const preKey = preState.key; 435 | 436 | if(!this._history.state){ // The user manually modifies the browser address bar 437 | let _popPushKey = preKey + 1; 438 | this._history.replaceState({[KEY_NAME]: _popPushKey}, ''); 439 | updatePreState(); 440 | this._setTra(''); 441 | this._replace(fullUrlParse(this.URL.getUrlByLocation()), '_popPush'); 442 | this._setModalCrumbsWhenChange(); 443 | return; 444 | } 445 | 446 | const total = this._getStepsTotal(preState); 447 | 448 | if(total > 0){ 449 | console.log('forward', total); 450 | console.error('Forward is disabled by history-navigation-vue'); 451 | this._isOmitForwardEvent = true; 452 | this.back(total); 453 | return; 454 | } 455 | 456 | const currState = getCurrState(); 457 | const currKey = currState.key; 458 | 459 | const page = this.stackMap[currKey]; 460 | if(!page && currState.modalKey){ 461 | this.back(currState.modalKey); 462 | return; 463 | } 464 | 465 | this._autoRemoveModal(); 466 | 467 | updatePreState(); 468 | 469 | if(preKey === currKey) { 470 | return; 471 | } 472 | 473 | // if(preKey === currKey){ 474 | // const modalKey = getCurrModaKey(); 475 | // const preModalKey = this.getLastModalKeyByCrumbs(preKey); 476 | // if(modalKey > preModalKey){ 477 | // this._isOmitForwardEvent = true; 478 | // this.back(modalKey - preModalKey); 479 | // return; 480 | // } 481 | // this.removeModal(); 482 | // // this._set2ModalCrumbsWhenChange(); 483 | // return; 484 | // } 485 | 486 | const compare = currKey - preKey; 487 | 488 | let backTra = this._whenPopTra; 489 | if(!backTra && compare === -1){ 490 | backTra = this._getBackTra(); 491 | // console.log('--------------- backTra ---------------', backTra); 492 | } 493 | this._setTra(backTra); 494 | this._whenPopTra = null; 495 | 496 | 497 | const newBehavior = { 498 | type: 'back', 499 | distance: compare, 500 | isPop: true 501 | } 502 | Object.assign(this.behavior, newBehavior); 503 | 504 | if(page){ 505 | Object.assign(this.currentPage, page); 506 | } else { 507 | this._setMapItem(currKey, fullUrlParse(this.URL.getUrlByLocation())); 508 | } 509 | this.uniteVue.nextTick(() => { 510 | this._clearAfter(); 511 | this._onRouted(); 512 | }) 513 | } 514 | 515 | 516 | History.prototype.destroy = function(){ 517 | if(isCreated){ 518 | if(this._popstateHandle){ 519 | this._window.removeEventListener('popstate', this._popstateHandle); 520 | } 521 | if(this._handleWinUnload){ 522 | this._window.removeEventListener('_handleWinUnload', this._handleWinUnload); 523 | } 524 | isCreated = false; 525 | } 526 | } 527 | 528 | History.prototype.handleWinUnload = function(){ 529 | this._saveModalCrumbs(); 530 | } 531 | 532 | 533 | 534 | Object.assign(History.prototype, modalPart.proto); 535 | Object.assign(History.prototype, BAEPart.proto); 536 | Object.assign(History.prototype, BackPart.proto); 537 | 538 | // History.prototype.fitVue$3 = function(){ // FIT_VUE_3_SWITCH 539 | // if(this.uniteVue.is3){ 540 | // let v; 541 | // ['stackMap', 'behavior', 'currentPage', '_tra', 'tabList', 'tabStackMap'].forEach(k => { 542 | // v = this[k]; 543 | // if(v){ 544 | // this[k] = vue$3Reactive(v) 545 | // } 546 | // }) 547 | // } 548 | // } 549 | 550 | export default History; 551 | 552 | // window.addEventListener('beforeunload', function(event){ 553 | // console.log('beforeunload') 554 | // event.preventDefault(); 555 | // event.returnValue = "Are you sure you want to exit?" 556 | // return 'beforeunload'; 557 | // }) -------------------------------------------------------------------------------- /src/navigator/libs/back.js: -------------------------------------------------------------------------------- 1 | import { getCurrentStateKey, getCurrState } from '../state-key'; 2 | import { _getTotalSteps } from '../../util'; 3 | export default { 4 | init(){ 5 | this._whenPopTra = null; 6 | this._whenPopInfo = null; 7 | this._whenBackPopInfo = null; 8 | }, 9 | proto: { 10 | _getStepsTotal(distState){ 11 | const currState = getCurrState(); 12 | const { key, modalKey } = currState; 13 | const distKey = distState.key; 14 | const distModalKey = distState.modalKey; 15 | let count = key - distKey; 16 | if(count === 0){ 17 | return modalKey - distModalKey; 18 | } 19 | const arr = this._modal_crumbs; 20 | let start, end; 21 | const isBack = count > 0; 22 | if(isBack){ 23 | start = distState; 24 | end = currState; 25 | } else { 26 | start = currState; 27 | end = distState; 28 | } 29 | let total = _getTotalSteps(arr, start, end); 30 | if(!isBack){ 31 | total = -total; 32 | } 33 | return total; 34 | }, 35 | // _getTotalFromPreState(){ 36 | // return this._getStepsTotal({key: 1, modalKey: 0}); 37 | // }, 38 | _backGetTo1Count(){ 39 | return this._getStepsTotal({key: 1, modalKey: 0}); 40 | }, 41 | back(_steps, tra){ 42 | // const key = getCurrentStateKey(); 43 | // const modalCount = this.get2ModalStepsTotal(); 44 | // console.log('modalCount', modalCount, key) 45 | // const total = key + modalCount; 46 | let steps = _steps || 1; 47 | if(steps < 1){ 48 | return; 49 | } 50 | // if(typeof steps === 'number' && steps > 0){ 51 | // if(total - steps < 1){ 52 | // steps = total - 1; 53 | // } 54 | // } 55 | // console.log('[back] steps', steps); 56 | this._whenPopTra = tra; 57 | if(steps){ 58 | this._history.go(-steps); 59 | } 60 | }, 61 | backToPage(_steps, tra){ 62 | const arr = this._modal_crumbs; 63 | console.log('[backToPage] _modal_crumbs', arr); 64 | if(!arr.length){ 65 | this.back(_steps, tra); 66 | return; 67 | } 68 | const key = getCurrentStateKey(); 69 | const distKey = key - _steps; 70 | let i = 0, v, count = 0; 71 | const max = arr.length; 72 | for(; i < max; i++){ 73 | v = arr[i]; 74 | if(v[0] > distKey){ 75 | count = count + v[1]; 76 | } else { 77 | break; 78 | } 79 | } 80 | this.back(_steps + count, tra); 81 | 82 | }, 83 | _backAndApply(steps, method, args, tra){ 84 | if(steps < 1){ 85 | return; 86 | } 87 | this._whenBackPopInfo = { 88 | method, 89 | args 90 | } 91 | this.back(steps, tra); 92 | }, 93 | 94 | _backToStartAndReplace(fullParse, behavior, tra){ 95 | const total = this._backGetTo1Count(); 96 | // console.log('_backToStartAndReplace', total, this._modal_crumbs); 97 | // return; 98 | if(total > 0){ 99 | this._backAndApply(total, '_replace', [fullParse, behavior], tra); 100 | } else if(total === 0){ 101 | this._setTra(tra); 102 | this._replace(fullParse, behavior); 103 | } else { 104 | console.error('_backToStartAndReplace not back', total); 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/navigator/libs/bae.js: -------------------------------------------------------------------------------- 1 | import { getCurrentStateKey } from '../state-key'; 2 | import { MODAL_BAE_KEY, MODAL_KEY_NAME } from '../../constant'; 3 | export default { 4 | proto: { 5 | _isBAEPage(){ 6 | if(this.BAE){ 7 | const key = getCurrentStateKey(); 8 | const page = this.stackMap[key]; 9 | if(key === 1 && page && (page.isHome || page.isTab)){ 10 | return true; 11 | } 12 | } 13 | return false; 14 | }, 15 | _isBAEPageByTK(tk){ 16 | if(this.BAE){ 17 | const key = getCurrentStateKey(); 18 | const page = this.pageMap[tk]; 19 | if(key === 1 && page && (page.isHome || page.isTab)){ 20 | return true; 21 | } 22 | } 23 | return false; 24 | }, 25 | _isNeedBAE(){ 26 | if(this._isBAEPage()){ 27 | const state = this._history.state; 28 | if(state && !state[MODAL_KEY_NAME]){ 29 | return true; 30 | } 31 | } 32 | return false; 33 | }, 34 | _autoBAE(){ 35 | const isN = this._isNeedBAE(); 36 | console.log('_autoBAE', isN); 37 | if(isN){ 38 | this.modal(MODAL_BAE_KEY); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/navigator/libs/modal.js: -------------------------------------------------------------------------------- 1 | import { getCurrentStateKey, getCurrModaKey, updatePreState } from '../state-key'; 2 | import { KEY_NAME, MODAL_BAE_KEY, MODAL_CRUMBS_KEY_NAME, MODAL_KEY_NAME } from '../../constant'; 3 | import { _cutOffAndPush } from '../../util'; 4 | // import { nativeDocument } from '../native'; 5 | 6 | export default { 7 | init(){ 8 | this._modal_crumbs = null; 9 | this._initModalCrumbs(); 10 | console.log('[_initModalCrumbs]', this._modal_crumbs); 11 | }, 12 | proto: { 13 | modal({component, propsData, parent, success}){ 14 | const isBAEModal = arguments[0] === MODAL_BAE_KEY; 15 | if(!isBAEModal){ 16 | this._autoBAE(); 17 | } 18 | const key = getCurrentStateKey(); 19 | const page = this.stackMap[key]; 20 | let modalKey = getCurrModaKey(); 21 | modalKey = modalKey + 1; 22 | this._history.pushState({[KEY_NAME]: key, [MODAL_KEY_NAME]: modalKey}, ''); 23 | this._setModalCrumbsWhenChange(); 24 | updatePreState(); 25 | 26 | const item = { 27 | key: modalKey, 28 | uid: _genModalKey() 29 | } 30 | page.modalList.push(item); 31 | if(isBAEModal){ 32 | item.isBAE = true; 33 | return; 34 | } 35 | const id = 'h_nav_modal_' + item.uid; 36 | if(component){ 37 | this.uniteVue.nextTick(() => { 38 | if(!item._isDestroy){ 39 | // const Cmpt = this.uniteVue.extend(component); 40 | // const cmpt = new Cmpt({ 41 | // el: '#' + id, 42 | // parent, 43 | // propsData 44 | // }); 45 | const cmpt = this.uniteVue.newComponent(component, { 46 | el: '#' + id, 47 | parent, 48 | propsData 49 | }); 50 | item._destoryCmpt = () => { 51 | this.uniteVue.destroy(cmpt); 52 | }; 53 | success && success(cmpt); 54 | } 55 | }); 56 | } 57 | return id; 58 | }, 59 | removeModal(){ 60 | const currKey = getCurrentStateKey(); 61 | const modalKey = getCurrModaKey(); 62 | const page = this.stackMap[currKey]; 63 | let arr; 64 | if(page){ 65 | arr = page.modalList.splice(modalKey); 66 | arr.forEach(item => { 67 | item._isDestroy = true; 68 | if(item._destoryCmpt){ 69 | item._destoryCmpt(); 70 | } 71 | }) 72 | } 73 | if(modalKey === 0){ 74 | if(this._isNeedBAE()){ 75 | if(arr && arr.length > 1){ 76 | console.log('[removeModal] step not 1', arr.length); 77 | this._autoBAE(); 78 | } else { 79 | this.BAE.onFirstTrigger(); 80 | setTimeout(() => { 81 | this._autoBAE(); 82 | }, this.BAE.maxInterval); 83 | } 84 | } 85 | } 86 | }, 87 | _autoRemoveModal(){ 88 | const key = getCurrentStateKey(); 89 | const page = this.stackMap[key]; 90 | const modalKey = getCurrModaKey(); 91 | if(page && page.modalList.length > modalKey){ 92 | this.removeModal(); 93 | } 94 | }, 95 | _setModalCrumbsWhenChange(){ 96 | const key = getCurrentStateKey(); 97 | const modalKey = getCurrModaKey(); 98 | this._setModalCrumbs(key, modalKey); 99 | }, 100 | _initModalCrumbs(){ 101 | const h = this._history; 102 | const state = Object.assign({}, h.state); 103 | const v = state[MODAL_CRUMBS_KEY_NAME]; 104 | if(v){ 105 | delete state[MODAL_CRUMBS_KEY_NAME]; 106 | this._history.replaceState(state, ''); 107 | this._modal_crumbs = _parse(v); 108 | } else { 109 | const modalKey = getCurrModaKey(); 110 | if(modalKey){ 111 | this._modal_crumbs = [[getCurrentStateKey(), modalKey]]; 112 | } else { 113 | this._modal_crumbs = []; 114 | } 115 | 116 | } 117 | }, 118 | _setModalCrumbs(stateKey, modalCount){ 119 | const arr = this._modal_crumbs; 120 | _cutOffAndPush(arr, stateKey, modalCount); 121 | }, 122 | 123 | _saveModalCrumbs(){ 124 | console.log('_saveModalCrumbs'); 125 | if(this._modal_crumbs.length){ 126 | const h = this._history; 127 | const state = h.state || {}; 128 | state[MODAL_CRUMBS_KEY_NAME] = _format(this._modal_crumbs); 129 | this._history.replaceState(state, ''); 130 | } 131 | }, 132 | getLastModalKeyByCrumbs(stateKey){ 133 | const crumbs = this._modal_crumbs; 134 | const last = crumbs[crumbs.length - 1]; 135 | if(last){ 136 | if(last[0] === stateKey){ 137 | return last[1]; 138 | } 139 | } 140 | return 0; 141 | } 142 | } 143 | } 144 | 145 | function _parse(str){ 146 | // STATE_KEY:MODAL_COUNT;... 147 | const arr = str.split(';'); 148 | const result = []; 149 | let i = 0, v; 150 | const MAX = arr.length; 151 | for(; i < MAX; i++){ 152 | v = arr[i].split(':'); 153 | result.push([Number(v[0]), Number(v[1])]); 154 | } 155 | return result; 156 | } 157 | 158 | function _format(arr){ 159 | return arr.map(function(v) { 160 | console.log('_format', v); 161 | return v.join(':'); 162 | }).join(';'); 163 | } 164 | 165 | 166 | let _modalKey = 0; 167 | function _genModalKey(){ 168 | _modalKey = _modalKey + 1; 169 | return _modalKey; 170 | } -------------------------------------------------------------------------------- /src/navigator/native.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | let nativeWindow, nativeHistory, nativeLocation; 4 | // , nativeRAF 5 | if(typeof window !== 'undefined'){ 6 | nativeWindow = window; 7 | nativeHistory = window.history; 8 | nativeLocation = window.location; 9 | // nativeRAF = window.requestAnimationFrame || setTimeout; 10 | } 11 | // if(typeof document !== 'undefined'){ 12 | // nativeDocument = document; 13 | // } 14 | // else { 15 | // // nativeRAF = function(cb){cb()}; 16 | // // nativeRAF = setTimeout; 17 | // } 18 | 19 | 20 | export {nativeWindow, nativeHistory, nativeLocation} 21 | -------------------------------------------------------------------------------- /src/navigator/navigator.js: -------------------------------------------------------------------------------- 1 | import History from './history'; 2 | 3 | export default function(opt){ 4 | const h = new History(opt); 5 | const obj = { 6 | _h: h, 7 | URL: h.URL, 8 | // vueIs3: h.uniteVue.is3, FIT_VUE_3_SWITCH 9 | GLOBAL_CONFIG: h._global 10 | }; 11 | 12 | ['push', 'back', 'backToPage', 'replace', 'relaunch', 'switchTab', 'modal'].forEach(k => { 13 | _bindToOther(obj, k, h); 14 | }); 15 | 16 | return obj; 17 | 18 | } 19 | 20 | function _bindToOther(obj, attr, h){ 21 | obj[attr] = function(){ 22 | h[attr].apply(h, arguments); 23 | } 24 | } -------------------------------------------------------------------------------- /src/navigator/state-key.js: -------------------------------------------------------------------------------- 1 | import { nativeHistory } from './native'; 2 | import { KEY_NAME, MODAL_KEY_NAME } from '../constant'; 3 | 4 | export function getCurrentStateKey () { 5 | const state = nativeHistory.state; 6 | if (state && typeof state[KEY_NAME] === 'number') { 7 | return state[KEY_NAME]; 8 | } 9 | return 1; 10 | } 11 | 12 | export function getCurrModaKey(){ 13 | const state = nativeHistory.state; 14 | if(state && typeof state[MODAL_KEY_NAME] === 'number'){ 15 | return state[MODAL_KEY_NAME]; 16 | } 17 | return 0; 18 | } 19 | 20 | export function genStateKey () { 21 | return getCurrentStateKey() + 1; 22 | } 23 | 24 | export function getCurrState() { 25 | return { 26 | key: getCurrentStateKey(), 27 | modalKey: getCurrModaKey() 28 | }; 29 | } 30 | 31 | let _preState = getCurrState(); 32 | 33 | export function getPreState() { 34 | return Object.assign({}, _preState); 35 | } 36 | 37 | function _setPreState(state) { 38 | _preState = Object.assign({}, state); 39 | } 40 | 41 | export function updatePreState() { 42 | _setPreState(getCurrState()); 43 | } 44 | 45 | // let _preKey = getCurrentStateKey(); 46 | // export function get2PreStateKey () { 47 | // return _preKey; 48 | // } 49 | 50 | // export function set2PreStateKey (key) { 51 | // _preKey = key; 52 | // } 53 | 54 | -------------------------------------------------------------------------------- /src/navigator/url.js: -------------------------------------------------------------------------------- 1 | import { nativeLocation } from './native'; 2 | function URL({isHashMode = true, base = ''}){ 3 | 4 | this.isHashMode = isHashMode; 5 | 6 | this._location = nativeLocation; 7 | if(isHashMode){ 8 | this.hashBase = this._location.pathname + this._location.search + '#'; 9 | } else { 10 | this.base = base; 11 | } 12 | 13 | } 14 | 15 | URL.prototype.getHashUrlByLocation = function(){ 16 | let url = this._location.hash; 17 | if(url.length > 0){ 18 | url = url.substr(1); 19 | } 20 | return url; 21 | } 22 | 23 | URL.prototype.getUrlByLocation = function(){ 24 | if(this.isHashMode){ 25 | return this.getHashUrlByLocation(); 26 | } 27 | let pathname = this._location.pathname; 28 | let i = pathname.indexOf(this.base); 29 | if(i === 0){ 30 | pathname = pathname.substr(this.base.length); 31 | } 32 | return pathname + this._location.search; 33 | } 34 | 35 | URL.prototype.getRouteByLocation = function(){ 36 | return urlParse(this.getUrlByLocation()); 37 | } 38 | 39 | URL.prototype.toLocationUrl = function(fullPath){ 40 | return this.isHashMode ? (this.hashBase + fullPath) : (this.base + fullPath); 41 | } 42 | 43 | 44 | export function urlParse(url){ 45 | let i = url.indexOf('?'); 46 | let path, qsString; 47 | if(i !== -1){ 48 | path = url.substr(0, i); 49 | qsString = url.substr(i + 1); 50 | } else { 51 | path = url; 52 | qsString = ''; 53 | } 54 | return { 55 | path, 56 | query: queryParse(qsString) 57 | } 58 | } 59 | /* path, fullPath, trimedPath, query */ 60 | export function fullUrlParse(userUrl){ 61 | let route, fullPath, trimedPath; 62 | if(typeof userUrl === 'string'){ 63 | route = urlParse(userUrl); 64 | fullPath = userUrl; 65 | } else { 66 | route = trimRoute(userUrl); 67 | fullPath = _urlStringify(route); 68 | } 69 | trimedPath = trimSlash(route.path); 70 | return Object.assign({ 71 | fullPath, 72 | trimedPath 73 | }, route); 74 | } 75 | 76 | function trimRoute(route){ 77 | if(route.path.indexOf('?') === -1){ 78 | return route; 79 | } 80 | let _route = urlParse(route.path); 81 | Object.assign(_route.query, route.query); 82 | return _route; 83 | } 84 | 85 | export function urlStringify(route){ 86 | return _urlStringify(trimRoute(route)); 87 | } 88 | 89 | function _urlStringify(route){ 90 | let queryStr = queryStringify(route.query); 91 | if(queryStr) { 92 | queryStr = '?' + queryStr; 93 | } 94 | return route.path + queryStr; 95 | } 96 | 97 | function queryParse(qsString){ 98 | const map = Object.create(null); 99 | if(!qsString || (typeof qsString === 'string' && qsString.length === 0)){ 100 | return map; 101 | } 102 | const arr = qsString.split('&'); 103 | let i = 0, len = arr.length, v; 104 | for(; i < len; i++){ 105 | v = arr[i]; 106 | v = v.split('='); 107 | map[v[0]] = decodeURIComponent(v[1]); 108 | } 109 | return map; 110 | } 111 | 112 | function queryStringify(obj){ 113 | if(!obj){ 114 | return ''; 115 | } 116 | let arr = []; 117 | Object.keys(obj).forEach(k => { 118 | arr.push(k + '=' + encodeURIComponent(obj[k])); 119 | }); 120 | return arr.join('&'); 121 | } 122 | 123 | // function pathJoin(str1, str2){ 124 | // return trimSlash(str1 + '/' + str2); 125 | // } 126 | 127 | export function trimSlash(pathStr){ 128 | let arr = pathStr.split('/'); 129 | arr = arr.filter(v => v !== ''); 130 | return arr.join('/'); 131 | } 132 | 133 | // function _joinBase(base, fullPath){ 134 | // if(fullPath[0] === '/'){ 135 | // return base + fullPath; 136 | // } 137 | // return base + '/' + fullPath; 138 | // } 139 | 140 | export default URL; -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export function noop() {} 2 | 3 | export function def(dest, src, key, defValue){ 4 | let v = src[key]; 5 | dest[key] = v === undefined ? defValue : v; 6 | } 7 | 8 | // export function defNew(defObj, src){ 9 | // const dest = Object.create(null); 10 | // const keys = Object.keys(defObj); 11 | // const len = keys.length; 12 | // let i = 0, k; 13 | // for(; i < len; i++){ 14 | // k = keys[i]; 15 | // def(dest, src, k, defObj[k]); 16 | // } 17 | // return dest; 18 | // } 19 | 20 | export function throwErr(msg){ 21 | throw new Error(`history-navigation-vue: ${msg}`); 22 | } 23 | 24 | export function getVueV(_Vue){ 25 | let v = _Vue.version; 26 | v = v.substr(0, v.indexOf('.')); 27 | return Number(v); 28 | } 29 | 30 | 31 | export function _cutOffAndPush(arr, stateKey, modalCount){ 32 | let i = 0; 33 | const len = arr.length; 34 | let v; 35 | for(; i < len; i++){ 36 | v = arr[i]; 37 | if(v[0] >= stateKey){ 38 | break; 39 | } 40 | } 41 | arr.splice(i, arr.length); 42 | if(modalCount){ 43 | arr.push([stateKey, modalCount]); 44 | } 45 | } 46 | 47 | export function _getModalSteps(arr, startState, endState){ 48 | console.log('startState', startState, 'endState', endState); 49 | let i = _findCrumbsIndex(arr, startState.key); 50 | 51 | const endIndex = _findCrumbsIndex(arr, endState.key); 52 | let count = 0; 53 | const startCrumb = arr[i]; 54 | if(startCrumb && startCrumb[0] === startState.key){ 55 | count = arr[i][1] - startState.modalKey; 56 | } 57 | count = count + endState.modalKey; 58 | i = i + 1; 59 | for(; i < endIndex; i++){ 60 | count = count + arr[i][1]; 61 | } 62 | return count; 63 | } 64 | 65 | export function _getTotalSteps(arr, startState, endState){ 66 | const modals = _getModalSteps(arr, startState, endState); 67 | return modals + (endState.key - startState.key); 68 | } 69 | 70 | export function _findCrumbsIndex(arr, stateKey){ 71 | let i = 0; 72 | const len = arr.length; 73 | for(; i < len; i++){ 74 | if(arr[i][0] >= stateKey){ 75 | break; 76 | } 77 | } 78 | return i; 79 | } -------------------------------------------------------------------------------- /ssr/conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | ssrBundlePath: __dirname 4 | } -------------------------------------------------------------------------------- /ssr/front-end/app.js: -------------------------------------------------------------------------------- 1 | // app.js 2 | import Vue from 'vue' 3 | import _createApp from '../../examples/create_app' 4 | export function createApp () { 5 | 6 | const app = _createApp(Vue, { 7 | urlIsHashMode: true 8 | }); 9 | 10 | // 返回 app 和 router 11 | return { app } 12 | } -------------------------------------------------------------------------------- /ssr/front-end/entry-server.js: -------------------------------------------------------------------------------- 1 | // entry-server.js 2 | import { createApp } from './app'; 3 | 4 | export default context => { 5 | // 因为有可能会是异步路由钩子函数或组件,所以我们将返回一个 Promise, 6 | // 以便服务器能够等待所有的内容在渲染前, 7 | // 就已经准备就绪。 8 | return new Promise((resolve, reject) => { 9 | const app = createApp(); 10 | // 设置服务器端 router 的位置 11 | app.$navigator.replace(context.url); 12 | 13 | resolve(app); 14 | }) 15 | } -------------------------------------------------------------------------------- /ssr/ssr-build.js: -------------------------------------------------------------------------------- 1 | // https://ssr.vuejs.org/zh/ 2 | 3 | var child_process = require('child_process'); 4 | const conf = require('../config'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const proDistPath = path.join(__dirname, '../dist'); 8 | const fsExt = require('fs-extra'); 9 | const contextConfig = require('./ssr-context-config'); 10 | 11 | module.exports = function (callback) { 12 | console.log('=============== ssr build 开始 ===============\n'); 13 | console.log('+++++++ 创建 ssr bundle... +++++++'); 14 | child_process.execSync('npm run ssr_build'); 15 | console.log('+++++++ 创建 ssr bundle 成功!+++++++\n'); 16 | callback(); 17 | 18 | const render = require('./ssr-render'); 19 | 20 | const routeMap = conf.routes.map; 21 | const routeKeys = Object.keys(routeMap); 22 | function loop (i) { 23 | if (i < routeKeys.length) { 24 | const key = routeKeys[i]; 25 | const context = Object.assign({}, routeMap[key]); 26 | if(contextConfig[key]){ 27 | Object.assign(context, contextConfig[key]); 28 | } 29 | // 不能为 undefined 30 | if ( context.ssrHeadScript === undefined) { 31 | context.ssrHeadScript = ''; 32 | } 33 | 34 | context.title = context.title + conf.routes.titleSuffix; 35 | context.url = key; 36 | render(context, (html) => { 37 | const name = context.url.replace(/^\//, ''); 38 | 39 | const distPath = path.join(proDistPath, name); 40 | fsExt.mkdirpSync(distPath); 41 | const filePath = path.join(distPath, 'index.html'); 42 | fs.writeFileSync(filePath, html); 43 | const stat = fs.statSync(filePath); 44 | console.log(name + '/index.html build 成功!', B2KB(stat.size)); 45 | loop (i + 1); 46 | }) 47 | } else { 48 | console.log('\n=============== ssr build 结束 ===============\n'); 49 | } 50 | } 51 | 52 | loop (0); 53 | } 54 | 55 | 56 | function B2KB(size){ 57 | return (size / 1024).toFixed(2) + 'KB'; 58 | } 59 | -------------------------------------------------------------------------------- /ssr/ssr-context-config.js: -------------------------------------------------------------------------------- 1 | // 只存在后端的数据。开发时需要 npm run build 后,在 npm run preview 查看。 2 | 3 | module.exports = { 4 | '/recall': { 5 | ssrHeadScript: `` 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ssr/ssr-render.js: -------------------------------------------------------------------------------- 1 | // server.js 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const conf = require('./conf'); 5 | const serverBundle = require(path.join(conf.ssrBundlePath, 'vue-ssr-server-bundle.json')); 6 | const { createBundleRenderer } = require('vue-server-renderer'); 7 | // const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf-8'); 8 | const renderer = createBundleRenderer(serverBundle, { 9 | runInNewContext: false, // 推荐 10 | // template // (可选)页面模板 11 | }) 12 | 13 | module.exports = function render(context, callback) { 14 | // 这里无需传入一个应用程序,因为在执行 bundle 时已经自动创建过。 15 | // 现在我们的服务器与应用程序已经解耦! 16 | renderer.renderToString(context, (err, html) => { 17 | // 处理异常…… 18 | if (err) { 19 | throw err; 20 | } 21 | callback(html); 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /ssr/webpack.config.js: -------------------------------------------------------------------------------- 1 | const {merge} = require('webpack-merge') 2 | const nodeExternals = require('webpack-node-externals') 3 | const baseConfig = require('../webpack.config.js') 4 | const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') 5 | const path = require('path'); 6 | const conf = require('./conf'); 7 | module.exports = merge(baseConfig, { 8 | // 将 entry 指向应用程序的 server entry 文件 9 | entry: path.join(__dirname, 'front-end/entry-server.js'), 10 | 11 | // 这允许 webpack 以 Node 适用方式(Node-appropriate fashion)处理动态导入(dynamic import), 12 | // 并且还会在编译 Vue 组件时, 13 | // 告知 `vue-loader` 输送面向服务器代码(server-oriented code)。 14 | target: 'node', 15 | 16 | // 对 bundle renderer 提供 source map 支持 17 | devtool: 'source-map', 18 | 19 | // 此处告知 server bundle 使用 Node 风格导出模块(Node-style exports) 20 | output: { 21 | libraryTarget: 'commonjs2', 22 | path: conf.ssrBundlePath 23 | }, 24 | 25 | // https://webpack.js.org/configuration/externals/#function 26 | // https://github.com/liady/webpack-node-externals 27 | // 外置化应用程序依赖模块。可以使服务器构建速度更快, 28 | // 并生成较小的 bundle 文件。 29 | externals: nodeExternals({ 30 | // 不要外置化 webpack 需要处理的依赖模块。 31 | // 你可以在这里添加更多的文件类型。例如,未处理 *.vue 原始文件, 32 | // 你还应该将修改 `global`(例如 polyfill)的依赖模块列入白名单 33 | allowlist: [/\.css$/, /\.vue$/] 34 | }), 35 | 36 | // 这是将服务器的整个输出 37 | // 构建为单个 JSON 文件的插件。 38 | // 默认文件名为 `vue-ssr-server-bundle.json` 39 | plugins: [ 40 | new VueSSRServerPlugin() 41 | ] 42 | }) -------------------------------------------------------------------------------- /test/e2e/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hezedu/history-navigation-vue/b68b1c59abb8dd4419d01cfc16ffc12b1cc9f311/test/e2e/.placeholder -------------------------------------------------------------------------------- /test/e2e/history.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | require('chromedriver'); 3 | const webDriver = require('selenium-webdriver'); 4 | const ROOT_URL = 'http://localhost:8080/history-navigation-vue/examples/'; 5 | 6 | const { By } = webDriver; 7 | const driver = new webDriver.Builder() 8 | .forBrowser('chrome') 9 | .build(); 10 | 11 | describe('History', function() { 12 | describe('#Hello World', function() { 13 | it('_h_n_key eq 1 when load', function(done) { 14 | 15 | driver.get(ROOT_URL + 'two-pages.html').then(() => { 16 | driver.executeScript("return history.state;").then((state) => { 17 | assert.deepEqual(state, {_h_n_key: 1}); 18 | }); 19 | }); 20 | done(); 21 | }); 22 | it('_h_n_key eq 2 after push', function(done) { 23 | 24 | driver.findElement(By.tagName('a')).click().then(() => { 25 | // setTimeout(() => { 26 | driver.executeScript("return history.state;").then((state) => { 27 | assert.deepEqual(state, {_h_n_key: 2}); 28 | done(); 29 | }); 30 | // }) 31 | 32 | }); 33 | 34 | }); 35 | it('_h_n_key eq 1 after back', function(done) { 36 | driver.navigate().back().then(() => { 37 | // setTimeout(() => { 38 | driver.executeScript("return history.state;").then((state) => { 39 | assert.deepEqual(state, {_h_n_key: 1}); 40 | done(); 41 | }); 42 | // }, 1000); 43 | 44 | }) 45 | }); 46 | }); 47 | 48 | after(() => { 49 | driver.quit(); 50 | }); 51 | }); -------------------------------------------------------------------------------- /test/unit/util.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | const vm = require('vm'); 3 | const fs = require('fs'); 4 | let source = fs.readFileSync('../src/util.js', 'utf-8'); 5 | source = source.replace(/export function ([\s\S]+?)\(/g, (mstr) => { 6 | const name = mstr.substring(mstr.lastIndexOf(' '), mstr.length - 1); 7 | return `function ${name}(`; 8 | 9 | }) 10 | eval(source); // _cutOffAndPush 11 | describe('Util', function() { 12 | 13 | describe('#_getTotalSteps', function() { 14 | it('total should eq 1', function() { 15 | const count = _getTotalSteps([], {key: 1, modalKey: 0}, {key: 2, modalKey: 0}); 16 | assert.equal(count, 1); 17 | }); 18 | }); 19 | 20 | describe('#_getModalSteps', function() { 21 | it('step should eq 0', function() { 22 | const count = _getModalSteps([], {key: 1, modalKey: 0}, {key: 2, modalKey: 0}); 23 | assert.equal(count, 0); 24 | }); 25 | it('step should eq 1', function() { 26 | const count = _getModalSteps([[1, 1]], {key: 1, modalKey: 0}, {key: 2, modalKey: 0}); 27 | assert.equal(count, 1); 28 | }); 29 | it('step should eq 2', function() { 30 | const count = _getModalSteps([[1, 1]], {key: 1, modalKey: 0}, {key: 2, modalKey: 1}); 31 | assert.equal(count, 2); 32 | }); 33 | it('step should eq 2 too', function() { 34 | const count = _getModalSteps([[1, 1], [2, 2]], {key: 1, modalKey: 0}, {key: 2, modalKey: 1}); 35 | assert.equal(count, 2); 36 | }); 37 | }); 38 | describe('#_findCrumbsIndex', function() { 39 | it('index should eq 0', function() { 40 | const index = _findCrumbsIndex([], 1); 41 | assert.equal(index, 0); 42 | }); 43 | it('index should eq 1', function() { 44 | const index = _findCrumbsIndex([[0], [2]], 1); 45 | assert.equal(index, 1); 46 | }); 47 | it('index should eq 1', function() { 48 | const index = _findCrumbsIndex([[0], [1]], 1); 49 | assert.equal(index, 1); 50 | }); 51 | }); 52 | describe('#_cutOffAndPush(arr, stateKey, modalCount)', function() { 53 | 54 | it('array length should eq 0', function() { 55 | const arr = []; 56 | _cutOffAndPush(arr, 1, 0) 57 | assert.equal(arr.length, 0); 58 | }); 59 | it('array length should eq 1', function() { 60 | const arr = []; 61 | _cutOffAndPush(arr, 1, 1); 62 | assert.deepEqual(arr, [[1, 1]]); 63 | }); 64 | it('array length should eq 0', function() { 65 | const arr = [[1, 1]]; 66 | _cutOffAndPush(arr, 1, 0) 67 | assert.equal(arr.length, 0); 68 | }); 69 | 70 | it('array length should eq 1', function() { 71 | const arr = [[1, 1], [2, 1], [3, 1]]; 72 | _cutOffAndPush(arr, 2, 0); 73 | 74 | assert.deepEqual(arr, [[1, 1]]); 75 | }); 76 | it('array length should eq 3', function() { 77 | const arr = [[1, 1], [2, 1]]; 78 | _cutOffAndPush(arr, 3, 1); 79 | assert.deepEqual(arr.length, 3); 80 | }); 81 | }); 82 | }); -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const isPro = process.argv.indexOf('--node-env=production') !== -1; 4 | 5 | process.env.NODE_ENV = 'production'; 6 | const NODE_ENV = process.env.NODE_ENV; 7 | const ESLintPlugin = require('eslint-webpack-plugin'); 8 | const path = require('path'); 9 | const webpack = require('webpack'); 10 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 11 | const TerserPlugin = require("terser-webpack-plugin"); 12 | const OptimizeCSSAssetsPlugin = require("css-minimizer-webpack-plugin"); 13 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 14 | 15 | 16 | 17 | const bundleName = 'history-navigation-vue'; 18 | const outputPath = path.join(__dirname, './dist'); 19 | const filename = isPro ? '[name].min' : '[name]'; 20 | var optimization; 21 | 22 | var cssRule = { 23 | test: /(\.css$)/, 24 | //use: ["css-loader", "postcss-loader", "sass-loader"] 25 | use: [ 26 | { 27 | loader: MiniCssExtractPlugin.loader 28 | }, 29 | 'css-loader', 30 | 'postcss-loader', 31 | // { 32 | // loader: 'sass-loader', 33 | // options: { 34 | // outputStyle: 'expanded' 35 | // } 36 | // } 37 | ] 38 | }; 39 | 40 | 41 | 42 | // ***************************** plugins ***************************** 43 | var plugins = [ 44 | new ESLintPlugin({ 45 | extensions: ['js', 'vue'] 46 | }), 47 | new VueLoaderPlugin(), 48 | new webpack.DefinePlugin({ 49 | 'process.env': { 50 | NODE_ENV: JSON.stringify(NODE_ENV) 51 | } 52 | }), 53 | ] 54 | var rules = [ 55 | { 56 | test: /\.js$/, 57 | loader: 'babel-loader', 58 | exclude: /node_modules/ 59 | }, 60 | { 61 | test: /\.vue$/, 62 | loader: 'vue-loader' 63 | }, 64 | cssRule 65 | ] 66 | // ***************************** 环境适配 ***************************** 67 | optimization = { 68 | minimize: isPro, 69 | minimizer: [ 70 | new TerserPlugin( 71 | // { 72 | // extractComments: { 73 | // condition: true, 74 | // filename: 'bundleName', 75 | // banner: (commentsFile) => { 76 | // return `My custom banner about license information ${commentsFile}`; 77 | // }, 78 | // } 79 | // } 80 | ), 81 | new OptimizeCSSAssetsPlugin({}) 82 | ] 83 | } 84 | 85 | plugins = plugins.concat([ 86 | new MiniCssExtractPlugin({ 87 | // Options similar to the same options in webpackOptions.output 88 | // both options are optional 89 | filename: filename + ".css" 90 | // chunkFilename: "chunk_[name]_[contenthash].css" 91 | }), 92 | 93 | new webpack.LoaderOptionsPlugin({ 94 | minimize: true 95 | }) 96 | 97 | ]); 98 | 99 | // -------- pre lint -------- 100 | // rules.unshift({ 101 | // enforce: "pre", 102 | // test: /(\.js|\.vue)$/, 103 | // include: [path.resolve(__dirname, "src")], 104 | // loader: "eslint-loader" 105 | // }) 106 | 107 | // ***************************** webpackConf ***************************** 108 | const webpackConf = { 109 | mode: NODE_ENV, 110 | optimization, 111 | context: path.join(__dirname, './src'), 112 | entry: { 113 | [bundleName]: "./bundle.js" 114 | }, 115 | output: { 116 | path: outputPath, 117 | filename: filename + '.js', 118 | library: 'historyNavigationVue', 119 | libraryTarget: 'umd' 120 | }, 121 | module: { 122 | rules 123 | }, 124 | resolve: { 125 | extensions: ['.js'] 126 | }, 127 | plugins: plugins, 128 | performance: { 129 | hints: false 130 | } 131 | }; 132 | 133 | if(!isPro){ 134 | 135 | const fs = require('fs'); 136 | _emptyDirSync(path.join(__dirname, 'dist')); 137 | function _emptyDirSync(dir){ 138 | const names = fs.readdirSync(dir); 139 | names.forEach(name => { 140 | fs.unlinkSync(path.join(dir, name)); 141 | }) 142 | } 143 | } else { 144 | console.log('build minimize files...'); 145 | } 146 | 147 | module.exports = webpackConf; 148 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const TerserPlugin = require("terser-webpack-plugin"); 6 | const OptimizeCSSAssetsPlugin = require("css-minimizer-webpack-plugin"); 7 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 8 | const setup = require('./server/setup'); 9 | const config = require('./config'); 10 | 11 | const NODE_ENV = process.env.NODE_ENV || 'development'; 12 | 13 | const isPro = NODE_ENV === 'production'; 14 | const {indexDir, 15 | bundleName, 16 | outputPath, 17 | publicPath} = require('./script/get_config')(); 18 | 19 | // console.log('indexDir', indexDir); 20 | // console.log('bundleName', bundleName); 21 | // console.log('outputPath', outputPath); 22 | // console.log('publicPath', publicPath); 23 | 24 | var optimization; 25 | 26 | var cssRule; 27 | if(!isPro){ //使用 命令weblack 28 | cssRule = { 29 | test: /(\.css$)/, 30 | use: ['style-loader', 'css-loader', 'postcss-loader'], 31 | // include: path.join(__dirname, './src') 32 | } 33 | 34 | }else{ 35 | cssRule = { 36 | test: /(\.css$)/, 37 | //use: ["css-loader", "postcss-loader", "sass-loader"] 38 | use: [ 39 | { 40 | loader: MiniCssExtractPlugin.loader 41 | }, 42 | 43 | "css-loader", 'postcss-loader' 44 | ] 45 | } 46 | 47 | } 48 | 49 | 50 | 51 | // ***************************** plugins ***************************** 52 | var plugins = [ 53 | new VueLoaderPlugin(), 54 | new webpack.DefinePlugin({ 55 | 'process.env': { 56 | NODE_ENV: JSON.stringify(NODE_ENV) 57 | } 58 | }), 59 | 60 | // create index.html 61 | new HtmlWebpackPlugin({ 62 | filename: path.join(indexDir + '/index.html'), 63 | template: path.join(__dirname, '/examples/index.ejs'), 64 | staticMap: setup.staticMap, 65 | data: { 66 | title: config.title 67 | } 68 | }) 69 | ] 70 | var rules = [ 71 | { 72 | test: /\.js$/, 73 | loader: 'babel-loader', 74 | exclude: /node_modules|dist/ 75 | }, 76 | { 77 | test: /\.vue$/, 78 | loader: 'vue-loader' 79 | }, 80 | cssRule 81 | ] 82 | // ***************************** 环境适配 ***************************** 83 | if (isPro) { 84 | optimization = { 85 | minimizer: [ 86 | new TerserPlugin(), 87 | new OptimizeCSSAssetsPlugin({}) 88 | ] 89 | } 90 | 91 | plugins = plugins.concat([//正式环境下压缩 92 | new MiniCssExtractPlugin({ 93 | // Options similar to the same options in webpackOptions.output 94 | // both options are optional 95 | filename: "[name]_[contenthash].css", 96 | chunkFilename: "chunk_[name]_[contenthash].css" 97 | }), 98 | 99 | // new webpack.optimize.TerserPlugin({ 100 | // compress: { 101 | // warnings: false, 102 | // }, 103 | // sourceMap: false, 104 | // output: { 105 | // comments: false, 106 | // } 107 | // }), 108 | 109 | new webpack.LoaderOptionsPlugin({ 110 | minimize: true 111 | }) 112 | 113 | ]); 114 | 115 | // -------- pre lint -------- 116 | // rules.unshift({ 117 | // enforce: "pre", 118 | // test: /(\.js|\.vue)$/, 119 | // include: [path.resolve(__dirname, "src")], 120 | // loader: "eslint-loader" 121 | // }) 122 | 123 | }; 124 | 125 | // ***************************** config ***************************** 126 | const webpackConf = { 127 | mode: NODE_ENV, 128 | optimization, 129 | context: path.join(__dirname, './examples'), 130 | entry: { 131 | z_app: "./app.js" 132 | }, 133 | output: { 134 | path: outputPath, 135 | publicPath, 136 | filename: bundleName, 137 | chunkFilename: 'chunk_' + bundleName 138 | }, 139 | module: { 140 | rules 141 | }, 142 | resolve: { 143 | extensions: ['.js'], 144 | // alias: { 145 | // '__ROOT__' : path.join(__dirname, './src') 146 | // } 147 | }, 148 | plugins: plugins, 149 | devServer: { 150 | port: config.port, 151 | onBeforeSetupMiddleware({app}){ 152 | setup(app) 153 | }, 154 | host: '0.0.0.0', 155 | static: { 156 | directory: indexDir, 157 | watch: false 158 | }, 159 | // contentBase: indexDir, 160 | hot: true, 161 | liveReload: false, 162 | // proxy: { 163 | // '/api': { 164 | // target: '', 165 | // changeOrigin: true 166 | // } 167 | // } 168 | }, 169 | performance: { 170 | hints: false 171 | } 172 | }; 173 | 174 | module.exports = webpackConf; 175 | --------------------------------------------------------------------------------