From bb8b5df8ad6bac8f96051cd900189c18f3010c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=AF=9B=E7=91=9E?= Date: Tue, 19 May 2020 13:42:07 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83his?= =?UTF-8?q?tory=E8=B7=AF=E7=94=B1=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++ README.md | 17 ++++--- build/devServer.js | 48 ++++++++++++-------- build/development.config.js | 6 +-- package.json | 2 +- src/components/RouterViewTransparent.ts | 24 ++++------ src/config/index.ts | 60 +++++++++---------------- src/pages/index/config/index.ts | 16 ++++--- src/pages/index/main.ts | 3 ++ src/pages/index/route/index.ts | 2 +- src/pages/other/config/index.ts | 12 +++++ src/pages/other/main.ts | 3 ++ src/pages/other/route/index.ts | 2 +- src/utils/ajax/index.ts | 39 +++++++++------- vue.config.js | 29 ++++++------ 15 files changed, 143 insertions(+), 124 deletions(-) create mode 100644 src/pages/other/config/index.ts 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'