├── src ├── assets │ ├── js │ │ └── common.js │ └── logo.png ├── api │ └── wechat │ │ └── index.js ├── config │ ├── request.js │ ├── app.js │ └── page.js ├── pages │ ├── index │ │ ├── main.js │ │ └── App.vue │ └── model │ │ ├── indexA │ │ ├── main.js │ │ └── App.vue │ │ └── indexB │ │ ├── main.js │ │ └── App.vue ├── utils │ ├── isTerminal.js │ ├── checkUrlParams.js │ ├── request.js │ ├── buildApiCode.js │ ├── common.js │ └── wechat.js └── components │ └── HelloWorld.vue ├── .env.dev ├── .env.pro ├── public ├── favicon.ico └── index.html ├── .env.pre ├── babel.config.js ├── .gitignore ├── jsconfig.json ├── vue.config.js ├── page.config.js ├── package.json └── README.md /src/assets/js/common.js: -------------------------------------------------------------------------------- 1 | import 'vant/lib/index.css'; 2 | -------------------------------------------------------------------------------- /.env.dev: -------------------------------------------------------------------------------- 1 | VUE_APP_ENV='Dev' 2 | VUE_APP_REQ_URL='https://xusenlin.com' 3 | 4 | -------------------------------------------------------------------------------- /src/api/wechat/index.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function wechatSignatureApi(){ 4 | 5 | } 6 | -------------------------------------------------------------------------------- /.env.pro: -------------------------------------------------------------------------------- 1 | NODE_ENV='production' 2 | VUE_APP_ENV='Pro' 3 | VUE_APP_REQ_URL='http://localhost:8080' 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/vue-multiple-pages/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xusenlin/vue-multiple-pages/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /.env.pre: -------------------------------------------------------------------------------- 1 | NODE_ENV='production' 2 | VUE_APP_ENV='Pre' 3 | VUE_APP_REQ_URL='http://localhost:8080' 4 | 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/config/request.js: -------------------------------------------------------------------------------- 1 | 2 | export const timeout = 2000 3 | 4 | export const requestRetry = 4 5 | 6 | export const requestRetryDelay = 800 7 | 8 | export const baseURL = process.env.VUE_APP_REQ_URL 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/config/app.js: -------------------------------------------------------------------------------- 1 | 2 | export const tokenKey = "ACCESS_TOKEN" 3 | 4 | export const userInfoKey = "USER_INFO" 5 | 6 | export const env = process.env.VUE_APP_ENV //Dev、Pre、Pro 7 | 8 | export const isDevEnv = env === "Dev" 9 | 10 | export const isPreEnv = env === "Pre" 11 | 12 | export const isProEnv = env === "Pro" 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "baseUrl": "./", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "@/*": [ 9 | "src/*" 10 | ] 11 | }, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "dom.iterable", 16 | "scripthost" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/index/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import "@/assets/js/common" 3 | import {createApp} from 'vue' 4 | import {initPage} from "@/utils/checkUrlParams.js" 5 | 6 | initPage().then(({pageName, pageParams}) => { 7 | window.$pageName = pageName; 8 | window.$pageParams = pageParams; 9 | createApp(App).mount('#app') 10 | }).catch(e => { 11 | console.log(e); 12 | }); 13 | -------------------------------------------------------------------------------- /src/pages/model/indexA/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import "@/assets/js/common" 3 | import {createApp} from 'vue' 4 | import {initPage} from "@/utils/checkUrlParams.js" 5 | 6 | initPage().then(({pageName, pageParams}) => { 7 | window.$pageName = pageName; 8 | window.$pageParams = pageParams; 9 | createApp(App).mount('#app') 10 | }).catch(e => { 11 | console.log(e); 12 | }); 13 | -------------------------------------------------------------------------------- /src/pages/model/indexB/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import "@/assets/js/common" 3 | import {createApp} from 'vue' 4 | import {initPage} from "@/utils/checkUrlParams.js" 5 | 6 | initPage().then(({pageName, pageParams}) => { 7 | window.$pageName = pageName; 8 | window.$pageParams = pageParams; 9 | createApp(App).mount('#app') 10 | }).catch(e => { 11 | console.log(e); 12 | }); 13 | -------------------------------------------------------------------------------- /src/config/page.js: -------------------------------------------------------------------------------- 1 | //这里做页面参数约定和说明,如果url没有携带requiredParams的参数则无法初始化页面 2 | module.exports = { 3 | demo: { 4 | title: "演示", 5 | requiredParams: {}, 6 | optionalParams: { 7 | userId: "url必须携带用户Id" 8 | } 9 | }, 10 | index: { 11 | title: "首页", 12 | requiredParams: {} 13 | }, 14 | indexA: { 15 | title: "首页A", 16 | requiredParams: {} 17 | }, 18 | indexB: { 19 | title: "首页B", 20 | requiredParams: {} 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/isTerminal.js: -------------------------------------------------------------------------------- 1 | const ua = window.navigator.userAgent; 2 | 3 | const isAndroid = /(Android);?[\s/]+([\d.]+)?/i.test(ua); 4 | const isIpad = /(iPad).*OS\s([\d_]+)/i.test(ua); 5 | const isIpod = /(iPod)(.*OS\s([\d_]+))?/i.test(ua); 6 | const isIphone = !isIpad && /(iPhone\sOS)\s([\d_]+)/i.test(ua); 7 | const isWechat = /micromessenger/i.test(ua); 8 | const isAlipay = /alipayclient/i.test(ua); 9 | 10 | export {isIphone,isWechat,isAlipay,isAndroid,isIpad,isIpod} 11 | -------------------------------------------------------------------------------- /src/pages/index/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /src/pages/model/indexA/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /src/pages/model/indexB/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 21 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vue/cli-service') 2 | 3 | const autoprefixer = require('autoprefixer'); 4 | const pxtoviewport = require('postcss-px-to-viewport-8-plugin'); 5 | const pagesConfig = require("./page.config.js"); 6 | 7 | 8 | module.exports = defineConfig({ 9 | transpileDependencies: true, 10 | pages: pagesConfig, 11 | css: { 12 | loaderOptions: { 13 | postcss: { 14 | postcssOptions: {plugins:[ 15 | autoprefixer(), 16 | pxtoviewport({ 17 | viewportWidth: 375 18 | }) 19 | ]} 20 | } 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | <% if("Pro" != process.env.VUE_APP_ENV){ %> 11 | 12 | 13 | <% } %> 14 | 17 | 18 | 19 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /page.config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const Config = require("./src/config/page.js"); 3 | 4 | let pageConfig = {}; 5 | 6 | function addPageConfig(path, dir) { 7 | if (isPage(path + "/" + dir)) { 8 | if (pageConfig.dir) { 9 | throw new Error("有名字重复的页面:" + dir); 10 | } 11 | let template = "public/index.html"; 12 | 13 | if (fs.existsSync(path + "/" + dir + "/index.html")) { 14 | template = path + "/" + dir + "/index.html"; 15 | } 16 | pageConfig[dir] = { 17 | entry: path + "/" + dir + "/main.js", 18 | filename: dir + ".html", 19 | path: dir, 20 | title: Config[dir] ? Config[dir].title : "", 21 | template: template 22 | }; 23 | } 24 | let fileOrDir = fs.readdirSync(path + "/" + dir); 25 | fileOrDir.forEach(function(file) { 26 | let newDir = path + "/" + dir; 27 | if (fs.statSync(newDir + "/" + file).isDirectory()) { 28 | addPageConfig(newDir, file); 29 | } 30 | }); 31 | } 32 | 33 | function isPage(dir) { 34 | return fs.existsSync(dir + "/main.js") && fs.existsSync(dir + "/App.vue"); 35 | } 36 | 37 | addPageConfig("src", "pages"); 38 | 39 | module.exports = pageConfig; 40 | -------------------------------------------------------------------------------- /src/utils/checkUrlParams.js: -------------------------------------------------------------------------------- 1 | import { getPageParams } from "./common.js"; 2 | import allPageConfig from "@/config/page.js"; 3 | 4 | let pageName = window.PAGE_PATH; //PAGE_PATH在public中定义。 5 | let pageConfig = allPageConfig[pageName] || {}; 6 | 7 | function validateParams(requiredParams = {}, pageParams) { 8 | let errorMsg = []; 9 | if (Object.keys(requiredParams).length === 0) return errorMsg; 10 | for (let key in requiredParams) { 11 | if (!pageParams[key]) { 12 | errorMsg.push({ name: key, desc: requiredParams[key] }); 13 | } 14 | } 15 | return errorMsg; 16 | } 17 | 18 | export function initPage() { 19 | let pageParams = getPageParams(); 20 | let errorMsg = validateParams(pageConfig.requiredParams, pageParams); 21 | return new Promise((resolve, reject) => { 22 | if (errorMsg.length !== 0) { 23 | console.table(errorMsg); 24 | document.body.innerHTML = `
25 | 页面路径出错,请尝试重新打开。 26 | 参数:${errorMsg.map(r=>(r.desc)).join("、")} 27 |
`; 28 | reject(errorMsg); 29 | } else { 30 | resolve({pageName, pageParams}); 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-multiple-pages", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vue-cli-service serve --mode dev", 7 | "build": "vue-cli-service build --mode pro", 8 | "build:dev": "vue-cli-service build --mode dev", 9 | "build:test": "vue-cli-service build --mode test", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.27.2", 14 | "good-storage": "^1.1.1", 15 | "vant": "^4.0.0-rc.6", 16 | "vue": "^3.2.13" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.12.16", 20 | "@babel/eslint-parser": "^7.12.16", 21 | "@vue/cli-plugin-babel": "~5.0.0", 22 | "@vue/cli-plugin-eslint": "~5.0.0", 23 | "@vue/cli-service": "~5.0.0", 24 | "core-js": "^3.8.3", 25 | "eslint": "^7.32.0", 26 | "eslint-plugin-vue": "^8.0.3", 27 | "postcss-px-to-viewport-8-plugin": "^1.1.5", 28 | "sass": "^1.54.5", 29 | "sass-loader": "^13.0.2" 30 | }, 31 | "eslintConfig": { 32 | "root": true, 33 | "env": { 34 | "node": true, 35 | "vue/setup-compiler-macros": true 36 | }, 37 | "extends": [ 38 | "plugin:vue/vue3-essential", 39 | "eslint:recommended" 40 | ], 41 | "rules": {} 42 | }, 43 | "browserslist": [ 44 | "> 1%", 45 | "last 2 versions", 46 | "not dead", 47 | "not ie 11" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import Axios from "axios"; 2 | import { getToken } from "@/utils/common.js"; 3 | import { showLoadingToast,closeToast } from "vant"; 4 | import { baseURL,timeout,requestRetryDelay,requestRetry } from "@/config/request"; 5 | 6 | const service = Axios.create({ 7 | baseURL, 8 | headers: { 9 | Accept: "*/*" 10 | }, 11 | timeout 12 | }); 13 | 14 | service.defaults.retry = requestRetry; 15 | service.defaults.retryDelay = requestRetryDelay; 16 | 17 | 18 | service.interceptors.request.use( 19 | config => { 20 | if (config.showLoading) { 21 | showLoadingToast({ 22 | message: '加载中...', 23 | forbidClick: true, 24 | }); 25 | } 26 | config.headers["Authorization"] = getToken(); 27 | return config; 28 | }, 29 | error => { 30 | closeToast() 31 | Promise.reject(error); 32 | } 33 | ); 34 | 35 | service.interceptors.response.use( 36 | res => { 37 | closeToast() 38 | if (res.status !== 200) { 39 | //Toast('数据返回出错'); 40 | return Promise.reject("响应非200!"); 41 | } else { 42 | if (res.data.code != 100000) { 43 | //统一处理错误 44 | //Toast(res.data.msg); 45 | return Promise.reject("error"); 46 | } 47 | return res.data.data; 48 | } 49 | }, 50 | error => { 51 | return Promise.reject(error); 52 | } 53 | ); 54 | 55 | export default service; 56 | 57 | 58 | export const transformRequest = [function (data) { 59 | let formData = new FormData() 60 | for (let key in data) { 61 | formData.append(key, data[key]) 62 | } 63 | return formData 64 | }]; 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vueMultiplePages 2 | 3 | ## 一个 Vue 多页面应用,适用于移动端不需要单页应用(SPA)的场景 4 | 5 | 6 | ``` 7 | yarn build:dev //打包开发环境 8 | yarn build:pre //打包测试环境 9 | yarn build // 打包正式环境 10 | ``` 11 | 12 | > 番外: [MareWood](https://github.com/xusenlin/MareWood) 是一个 Go 开发的轻量级前端部署工具,可以很灵活的配置各种打包部署环境并提供访问,特别是远程的时候,方便后端和测试使用,草鸡好用。 13 | 14 | 本人做了很多H5相关的项目,如 M站、微信活动页面、原生App内嵌H5页面(jsBridge交互),从实践过程中,真的不建议大家在这部分做单页应用(SPA),首先微信里面的H5页面 15 | 在安卓和ios的url机制不一样,因此会导致两边的微信签名需要针对不同平台处理,当项目越来越大的时候,从其他应用跳转到这个应用的时候,即使你只使用其中一个页面,但是可能需要你去处理路由 16 | 和权限,页面的复用性没有多页面的好。使用多页面的话每个页面之间没有多余的联系,我可以配置好每个页面的标题,每个页面的url必须携带的参数等,页面之间的跳转可以直接 17 | `window.location.href = `也可以通过 jsBridge(在原生app的环境)去跳转。 18 | 19 | # 更新日志 20 | 21 | ### 第三次重构(2022.08)最新vueCli v5.0.8 + Vue(