diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7609d13..418227e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# 更新日志
+## v 1.2.18
+
+- 完善多SPA history 路由支持
+
## v 1.2.17
- 完善多SPA支持
diff --git a/README.md b/README.md
index ed8c3dc..5f0fb9d 100644
--- a/README.md
+++ b/README.md
@@ -328,6 +328,7 @@ yarn vue-cli-service help # [命令] : 比如 yarn vue-cli-service help test:e2e
- 尽量**不要使用全局注册**(插件/组件/指令/混入等)以优化性能并且代码更清晰、更易维护
- 尽量**按照依赖库的文档描述**来使用她, 从其源码(src)引入模块(css/scss/.../js/mjs/ts/jsx/tsx/vue), 将可能**不会被处理**且更可能随版本更新改变, 需要时可以从其构建后的 lib/dist 等目录引入或者增加一些配置(需要了解模块解析及转码规则和相关插件, 不推荐)
- 若开发环境出现缓存相关错误信息导致热更新慢, 可以删除 `node_modules/.cache` 文件夹再试
+- 路由路径不应出现符号 `.` , 以方便 `history 路由` 模式开发/部署
### 风格建议
@@ -656,8 +657,8 @@ yarn vue-cli-service help # [命令] : 比如 yarn vue-cli-service help test:e2e
```
- 所有视图组件可接收props:`route`代替`this.$route`, 区别是: **只在首次进入当前视图或当前视图url发生变化时改变**
-- 路由视图不需要被缓存的, 可以在meta申明/`deactivated`钩子销毁实例(`this.$destroy()`)或`activated`钩子进行更新
-- 为避免渲染错误, 请务必为循环创建的组件**加上 `key`**, 特别是 `tsx/ts/jsx/js` 中
+- **路由视图**不需要被缓存的, 可以在`meta`申明/`deactivated`钩子销毁实例(`this.$destroy()`)或`activated`钩子进行更新
+- 为避免渲染错误, 请务必为 循环创建的组件 **加上 `key`**, 需要特别注意 `tsx/ts/jsx/js` 文件(没有代码提示)
### 配置和优化
@@ -699,19 +700,21 @@ yarn vue-cli-service help # [命令] : 比如 yarn vue-cli-service help test:e2e
- 开启 `gzip` 压缩, 并重用已有 `gz` 文件 `gzip_static on;`
- 缓存静态资源(html 可减少缓存时间)
- [HTTP2 Server Push](https://www.nginx.com/blog/nginx-1-13-9-http2-server-push) 服务器推送, 需要 `nginx` 版本**1.13.9**及以上, [文档链接](http://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_push_preload)
-- 多个SPA history路由部署, 只能一个SPA一个location了么(待运维大佬解决)?
+- 多个SPA(即 `html` 文件)以**history路由**(访问url不以`#`号标识)部署:
+ - 一个 `html` 一个 location 或 待运维大佬完善(示例如下)
+ - 按照注释修改**每个** `html` 的配置, 其中 `base` 改为对应访问路径
配置示例( `nginx.conf` 文件, `xxx` 换成对应值):
```bash
http {
- include /etc/nginx/mime.types;
+ include xxx/mime.types;
default_type application/octet-stream;
# log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
- # access_log /var/log/nginx/access.log main;
+ # access_log xxx/access.log main;
sendfile on;
# tcp_nopush on;
@@ -771,7 +774,7 @@ http {
# error_page 404 /404.html; # 未知页
location / {
- # rewrite ^/(?:path|path-alias)/(.*)$ /$1 last; # 兼容某些路由
+ # rewrite ^/(?:path|path-alias)/(.*)$ /$1 last; # 兼容某些url
# 设置静态资源缓存(文件名已带内容哈希了)
if ($uri ~ .*\.(?:js|css|jpg|jpeg|gif|png|ico|gz|svg|svgz|ttf|eot|mp4)$) {
expires 7d; # d: 天
@@ -787,7 +790,7 @@ http {
set $u /; # for 多页history路由 其他location: ^/location([^/]+)
if ($uri ~ ^/([^/]+)) {
- set $u $1.html; # 待测试
+ set $u $1.html; # 待测试并完善
}
try_files $uri $uri/ $uri.html $u / =404;
# try_files $uri $uri/ $uri.html /location$u /location =404;
diff --git a/build/devServer.js b/build/devServer.js
index 4dffa47..5728763 100644
--- a/build/devServer.js
+++ b/build/devServer.js
@@ -8,7 +8,7 @@ module.exports = function(ENV, PAGES) {
const TARGET = 'PROXY_TARGET'
const FIELD = ENV.PROXY_FIELD
const REG_BASE = /^BASE_PATH(\d*)$/
- const REG_URL = /^((?:http|ws)s?:\/\/)[^:/]+(.*)/
+ const REG_URL = /^((?:http|ws)s?:\/\/)[^:/]+(.*)$/
const removeField = (url, field) =>
url.replace(
@@ -73,8 +73,11 @@ module.exports = function(ENV, PAGES) {
}
// http2 应该是不能配置了
- const REG_SLASHES = /\/+/g
- const REG_FILES = /\..+$/
+ const REG_SPA = /^\/([^/]+)(.*)$/
+ const REG_HTML = /\.html$/
+ const REG_FILES = /[^/]+\.[^/]+$/
+ const REG_PATH = /^(?:http|ws)s?:\/\/[^/]+(.*)$/
+ const REG_SLASHES = /\/+/
return {
host,
port,
@@ -84,26 +87,35 @@ module.exports = function(ENV, PAGES) {
overlay: { errors: true }, // lint
openPage: ENV.DEV_SERVER_PAGE || '',
historyApiFallback: {
+ // index: '/index.html',
rewrites: [
{
// SPA可省略.html 支持history路由(路径不能有'.', 因为用REG_FILES匹配文件)
- // TODO: 精确匹配已有资源
from: /./,
- to({ parsedUrl: { pathname, search } }) {
- search || (search = '')
- const paths = pathname.replace(ENV.BASE_URL, '').split('/')
- paths[0] || paths.shift()
- const entry = paths.shift()
+ to(context) {
+ const parsedUrl = context.parsedUrl
+ const search = parsedUrl.search || ''
+ let pathname = REG_SPA.exec(parsedUrl.pathname)
- return (
- (PAGES.includes(entry)
- ? `${ENV.BASE_URL}/${
- paths.length && REG_FILES.test(pathname)
- ? paths.join('/')
- : `${entry}.html`
- }`.replace(REG_SLASHES, '/')
- : pathname) + search
- )
+ const entry = pathname[1].replace(REG_HTML, '')
+ if (PAGES[entry]) {
+ pathname = pathname[2]
+ if (REG_FILES.test(pathname)) {
+ let referer = context.request.headers.referer
+ if (referer) {
+ pathname = parsedUrl.pathname.split(REG_SLASHES)
+ referer = referer.replace(REG_PATH, '$1').split(REG_SLASHES)
+ while (pathname[0] === referer[0]) {
+ pathname.shift()
+ referer.shift()
+ }
+ return '/' + pathname.join('/') + search
+ }
+ return pathname + search
+ }
+ return `/${entry}.html${search}`
+ }
+ return parsedUrl.pathname + search
},
},
],
diff --git a/build/development.config.js b/build/development.config.js
index 98d07cb..6860ce4 100644
--- a/build/development.config.js
+++ b/build/development.config.js
@@ -10,9 +10,9 @@
* @param {chainWebpack} config 配置对象
* https://github.com/neutrinojs/webpack-chain#getting-started
*/
-module.exports = function(config) {
+module.exports = function(config, ENV) {
// https://webpack.js.org/configuration/devtool/#devtool
- config.devtool(process.env.DEV_TOOL || 'eval')
+ config.devtool(ENV.DEV_TOOL || 'eval')
/// 避免同名.vue文件sourceMap冲突 ///
// https://webpack.js.org/configuration/output/#outputdevtoolmodulefilenametemplate
// config.output.devtoolFallbackModuleFilenameTemplate(
@@ -35,7 +35,7 @@ module.exports = function(config) {
// return `webpack://${info.namespace}/${fileName}`
// })
- // config.output.ecmaVersion(+process.env.ES_VERSION || 6) // WIP
+ // config.output.ecmaVersion(+ENV.ES_VERSION || 6) // WIP
/// 文件监听 ///
config.watchOptions({ ignored: /node_modules/ })
diff --git a/package.json b/package.json
index 1cf4fbf..4dec857 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "vue-tpl",
- "version": "1.2.17",
+ "version": "1.2.18",
"private": false,
"description": "vue + vuex + vue router + TypeScript(支持 JavaScript) 模板",
"author": "毛瑞 ",
diff --git a/src/components/RouterViewTransparent.ts b/src/components/RouterViewTransparent.ts
index 26c885a..f6c8ec2 100644
--- a/src/components/RouterViewTransparent.ts
+++ b/src/components/RouterViewTransparent.ts
@@ -16,7 +16,7 @@ import getKey from '@/utils/getKey'
// import(/* webpackChunkName: "ihOne" */ './ModuleOne')
// )
-/** 透明分发路由(支持嵌套)
+/** 透明分发路由(支持嵌套), props: { max: number }
* 可以给个key防止复用:
*
*
@@ -24,9 +24,9 @@ import getKey from '@/utils/getKey'
*/
export default {
name: 'RVT',
- props: ['route'],
+ props: ['route', 'max'],
data() {
- return { d: 0 } // 是否失活/离开
+ return { d: 0 } // d: 是否失活/离开
},
beforeRouteUpdate(this: any, to, from, next) {
this.d = 0
@@ -47,22 +47,14 @@ export default {
return this.n
}
+ let max = this.max
+ max > 1 || (max = CONFIG.subPage > 1 ? CONFIG.subPage : 1)
const meta = (this.route || this.$route).meta
+ meta.k || (meta.k = getKey('v'))
return (this.n = h(
'KeepAlive',
- {
- props: {
- exclude: this.$router.$.e,
- max: CONFIG.subPage > 1 ? CONFIG.subPage : 1,
- },
- },
- [
- h(
- 'RouterView',
- { key: meta.k || (meta.k = getKey('v')) },
- this.$slots.default
- ),
- ]
+ { props: { exclude: this.$router.$.e, max: max } },
+ [h('RouterView', { key: meta.k }, this.$slots.default)]
))
},
} as Component
diff --git a/src/config/index.ts b/src/config/index.ts
index 2aebd27..30fd81b 100644
--- a/src/config/index.ts
+++ b/src/config/index.ts
@@ -6,9 +6,9 @@
export default {
/*! 【全局配置(时间单位ms)】 */
- /* !【↓应用跳转配置↓】history路由必须绝对路径 */
- /*! 索引页 */
- /** 索引页 */
+ /*! 【↓ SPA配置 ↓】history路由必须绝对路径 */
+ /*! 首页 */
+ /** 首页 */
index: './',
/*! 登录页 */
@@ -26,16 +26,26 @@ export default {
/*! 错误页 */
/** 错误页 */
error: '50x',
+ /*! 【↑ SPA配置 ↑】 */
- /* !【↑应用跳转配置↑】 */
-
- /*! 接口地址(hash路由建议相对路径, 比如'api') */
- /** 接口地址(hash路由建议相对路径, 比如'api') */
- baseUrl: process.env.BASE_PATH,
-
- /*! 网站路径, history路由必须/开头 */
- /** 网站路径, history路由必须/开头 */
- base: '',
+ /** 去指定SPA
+ * @param id SPA ID, 见this键值
+ *
+ * falsy: 去登录页
+ *
+ * string: 去指定页
+ *
+ * 不存在的id: 未知页
+ * @param query 查询参数 自己拼 ?foo=0&bar=1#hash...
+ */
+ g(id?: string, search?: string) {
+ try {
+ window.stop() // 停止加载资源
+ } catch (error) {}
+ location.href =
+ (id ? (this as any)[id] || this.notFind : this.login) + (search || '')
+ throw 0 // eslint-disable-line no-throw-literal
+ },
/*! 接口请求超时 0表示不限制 */
/** 接口请求超时 0表示不限制 */
@@ -49,14 +59,6 @@ export default {
/** 全局接口响应缓存最大存活时间 */
apiCacheAlive: 3 * 1000,
- /*! token cookie 字段 */
- /** token cookie 字段 */
- cookie: 'Authorization',
-
- /*! token head 字段 */
- /** token head 字段 */
- head: 'Authorization',
-
/*! 身份有效期(取与服务端有效期的最小值) */
/** 身份有效期(取与服务端有效期的最小值) */
tokenAlive: 2 * 60 * 60 * 1000,
@@ -72,22 +74,4 @@ export default {
/*! 最大页面缓存时间 */
/** 最大页面缓存时间 */
pageAlive: 30 * 1000,
-
- /** 去指定页
- * @param id SPA ID, 见this键值
- * falsy: 去登录页
- * string: 去指定页
- * 不存在的id: 未知页
- */
- g(id?: string) {
- if (id) {
- location.href = (this as any)[id] || this.notFind
- } else {
- try {
- window.stop() // 停止加载资源
- } catch (error) {}
- location.href = this.login
- throw 0 // eslint-disable-line no-throw-literal
- }
- },
}
diff --git a/src/pages/index/config/index.ts b/src/pages/index/config/index.ts
index 0d2ae2d..413092b 100644
--- a/src/pages/index/config/index.ts
+++ b/src/pages/index/config/index.ts
@@ -1,10 +1,14 @@
-/*
- * @Description: index页全局配置
- * @Author: 毛瑞
- * @Date: 2019-07-08 17:00:16
- */
+/** SPA 配置 */
export default {
- /*! 【index页配置】 */
+ /*! 【↓ history路由必须绝对路径 ↓】 */
+ /*! 网站路径 */
+ /** 网站路径 */
+ base: '',
+
+ /*! 接口地址 */
+ /** 接口地址 */
+ baseUrl: process.env.BASE_PATH,
+ /*! 【↑ history路由必须绝对路径 ↑】 */
/*! 图表重绘间隔(ms) */
/** 图表重绘间隔(ms)
diff --git a/src/pages/index/main.ts b/src/pages/index/main.ts
index c5c5c78..cf9deca 100644
--- a/src/pages/index/main.ts
+++ b/src/pages/index/main.ts
@@ -8,7 +8,10 @@ import router from './router'
import store from './store'
import App from './App'
+import CONFIG from './config'
import mount from '@/functions/main'
+import { setBase } from '@/utils/ajax'
import './registerServiceWorker'
+setBase(CONFIG.baseUrl)
mount(App, router, store)
diff --git a/src/pages/index/route/index.ts b/src/pages/index/route/index.ts
index dff00c2..1a09864 100644
--- a/src/pages/index/route/index.ts
+++ b/src/pages/index/route/index.ts
@@ -5,7 +5,7 @@
*/
import { RouterOptions } from 'vue-router'
-import CONFIG from '@/config'
+import CONFIG from '../config'
import { home, about } from '@index/views'
export default {
diff --git a/src/pages/other/config/index.ts b/src/pages/other/config/index.ts
new file mode 100644
index 0000000..ab846b2
--- /dev/null
+++ b/src/pages/other/config/index.ts
@@ -0,0 +1,12 @@
+/** SPA 配置 */
+export default {
+ /*! 【↓ history路由必须绝对路径 ↓】 */
+ /*! 网站路径 */
+ /** 网站路径 */
+ base: '',
+
+ /*! 接口地址 */
+ /** 接口地址 */
+ baseUrl: process.env.BASE_PATH,
+ /*! 【↑ history路由必须绝对路径 ↑】 */
+}
diff --git a/src/pages/other/main.ts b/src/pages/other/main.ts
index c5c5c78..cf9deca 100644
--- a/src/pages/other/main.ts
+++ b/src/pages/other/main.ts
@@ -8,7 +8,10 @@ import router from './router'
import store from './store'
import App from './App'
+import CONFIG from './config'
import mount from '@/functions/main'
+import { setBase } from '@/utils/ajax'
import './registerServiceWorker'
+setBase(CONFIG.baseUrl)
mount(App, router, store)
diff --git a/src/pages/other/route/index.ts b/src/pages/other/route/index.ts
index 0c17336..6d13601 100644
--- a/src/pages/other/route/index.ts
+++ b/src/pages/other/route/index.ts
@@ -5,7 +5,7 @@
*/
import { RouterOptions } from 'vue-router'
-import CONFIG from '@/config'
+import CONFIG from '../config'
import { home, about } from '@other/views'
export default {
diff --git a/src/utils/ajax/index.ts b/src/utils/ajax/index.ts
index 1c4b1ab..4516b2c 100644
--- a/src/utils/ajax/index.ts
+++ b/src/utils/ajax/index.ts
@@ -19,7 +19,6 @@ import WS from './websocket'
// 默认请求配置 https://github.com/axios/axios#config-defaults
clone(AXIOS.defaults, {
- baseURL: CONFIG.baseUrl, // 请求前缀(相对路径时添加)
timeout: CONFIG.timeout, // 超时
// 从cookie设置请求头
@@ -45,10 +44,17 @@ clone(AXIOS.defaults, {
// alive: 0, // 该请求响应缓存最大存活时间 默认:CONFIG.apiCacheAlive
})
-/** 请求队列 */
-const requestQueue = new Memory()
-/** 【get】响应缓存 */
-const dataStore = new Memory(CONFIG.apiMaxCache, CONFIG.apiCacheAlive)
+/** 【debug】带上特定查询字段 */
+let SEARCH: IObject | undefined
+location.search
+ .replace(/\/$/, '')
+ .replace(
+ new RegExp(`[?&](${process.env.SEARCH_FIELD})=([^&]*)`, 'g'),
+ (match, field, value) => {
+ value && ((SEARCH || (SEARCH = {}))[field] = value)
+ return match
+ }
+ )
/** 取消请求 https://github.com/axios/axios#cancellation
* 创建一次token只能用对应的cancel一次, 不能复用
@@ -57,6 +63,13 @@ const CancelToken = AXIOS.CancelToken
/** 是否被取消 */
const isCancel = AXIOS.isCancel
+/** 设置【全局】请求路径
+ * @param baseURL 所有非http开头的请求添加的前缀
+ */
+function setBase(baseURL: string) {
+ AXIOS.defaults.baseURL = baseURL
+}
+
/** 全局请求头配置【只用于携带token等】 */
let HEAD = AXIOS.defaults.headers || (AXIOS.defaults.headers = {})
HEAD = HEAD.common || (HEAD.common = {})
@@ -81,18 +94,6 @@ function setHEAD(
}
}
-/** 【debug】带上特定查询字段 */
-let SEARCH: IObject | undefined
-location.search
- .replace(/\/$/, '')
- .replace(
- new RegExp(`[?&](${process.env.SEARCH_FIELD})=([^&]*)`, 'g'),
- (match, field, value) => {
- value && ((SEARCH || (SEARCH = {}))[field] = value)
- return match
- }
- )
-
/** 获取url (直接使用url的情况, 比如验证码、下载、上传等, 添加BaseUrl、调试参数等)
* @param {string} url
* @param {IObject} params 查询参数
@@ -142,6 +143,9 @@ function getKey(url: string, params?: IObject) {
return part[0] + query
}
+const requestQueue = new Memory()
+const dataStore = new Memory(CONFIG.apiMaxCache, CONFIG.apiCacheAlive)
+
/** 发起请求
* @param {String} url 请求地址
* @param {String} method http方法
@@ -324,6 +328,7 @@ function cancel(reason?: string) {
export {
CancelToken,
isCancel,
+ setBase,
HEAD,
setHEAD,
getUri,
diff --git a/vue.config.js b/vue.config.js
index f2a4539..4fee617 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -3,13 +3,6 @@
* @Author: 毛瑞
* @Date: 2019-06-18 16:18:18
*/
-const ENV = process.env // 环境变量
-const isProd = ENV.NODE_ENV === 'production' // 是否生产环境
-const pages = require('./build/pages')(isProd, ENV._ENTRIES) // 自动检测并返回页面入口设置
-const PAGE_NAMES = Object.keys(pages)
-
-const ALIAS = {} // 别名字典
-// 输出图形
const FIGURE = require('./build/figure')
// eslint-disable-next-line no-console
console.log(
@@ -18,7 +11,11 @@ console.log(
'm' +
FIGURE[(Math.random() * FIGURE.length) | 0] +
'\33[0m' // eslint-disable-line no-octal-escape
-)
+) // 输出图形
+const ENV = process.env
+const isProd = ENV.NODE_ENV === 'production'
+const pages = require('./build/pages')(isProd, ENV._ENTRIES) // SPAs
+const ALIAS = {} // 别名字典
/// 【配置项】https://cli.vuejs.org/zh/config ///
module.exports = {
@@ -36,7 +33,7 @@ module.exports = {
css: require('./build/css')(isProd, ALIAS, ENV),
/// 【开发服务器配置】 ///
- devServer: require('./build/devServer')(ENV, PAGE_NAMES),
+ devServer: require('./build/devServer')(ENV, pages),
/// 【webpack配置】 ///
// https://github.com/neutrinojs/webpack-chain#getting-started
@@ -51,7 +48,7 @@ module.exports = {
env = JSON.parse(ENV._ALIAS)
} catch (error) {}
env = {
- [prefix + 'ENTRIES']: JSON.stringify(PAGE_NAMES),
+ [prefix + 'ENTRIES']: JSON.stringify(Object.keys(pages)),
[prefix + 'ALIAS']: JSON.stringify(
require('./build/alias')(pages, config, ALIAS, env)
),
@@ -65,12 +62,6 @@ module.exports = {
}
config.plugin('define').use(require('webpack').DefinePlugin, [env])
- /// 不处理的依赖库 ///
- // 在html模板引入了会创建全局变量的js后可以设置以在src中使用这个全局变量
- // config.externals({
- // global: 'global',
- // })
-
/// web workers 支持 ///
config.module
.rule('web workers')
@@ -85,6 +76,12 @@ module.exports = {
})
.after('0') // merge名字变数组索引了
+ /// 不处理的依赖库 ///
+ // 在html模板引入了会创建全局变量的js后可以设置以在src中使用这个全局变量
+ // config.externals({
+ // global: 'global',
+ // })
+
/// 【不同环境配置】 ///
require(isProd
? './build/production.config'