├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public └── favicon.ico ├── src ├── assets │ └── .gitkeep ├── components │ └── .gitkeep └── pages │ ├── index │ ├── App.vue │ ├── index.html │ └── main.js │ ├── page2 │ ├── App.vue │ ├── index.html │ └── main.js │ └── test │ └── page │ ├── App.vue │ ├── index.html │ └── main.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | package-lock.json 4 | /dist 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 如果你是找一个多入口的脚手架,很抱歉并不能给你提供帮助,请直接使用[Nuxt](https://github.com/nuxt/nuxt.js)开启你的旅程。 4 | 5 | 本文主要是总结使用vue-cli配置多入口的一些经验,用于解决当你需要在单页应用中挂载不同的入口遇到的问题。 6 | 7 | 需要注意的是,在多页应用中子路由不支持history路由的形式,可以使用hash。 8 | 9 | ## 特性 10 | - 🎉 Vue 3 & @vue/cli 4,保持最新的 Vue 依赖 11 | - ✅ 自动生成入口,所有入口以目录的形式存放于src/pages下,并在第一层目录需包含index.html文件 12 | - ✅ 支持嵌套路由,如在pages目录下存在/test/page1/index.html,访问路径为:/test/page1 13 | - ✅ 根目录挂载,目录名为index会挂载到根目录下,而不是通过/index访问 14 | 15 | ## 什么是多页应用 16 | 17 | 单页应用(SPA)往往只含有包含一个主入口文件与index.html,页面间切换通过局部刷新资源来完成。而在多页应用中,我们会为每个HTML文档文件都指定好一个JS入口,这样一来当页面跳转时用户会获得一个新的HTML文档,整个页面会重新加载。 18 | 19 | 单页应用、多页应用的优劣势在此就不进行分析了,总而言之,多页架构模式暂时是无法取代的,如果尝试把几十个不关联的页面做成一个,那么开发成本会非常大的,**Not every app has to be an SPA**。 20 | 21 | ## 初始化项目 22 | 23 | 首先我们安装好vue-cli脚手架,并初始化一个默认工程: 24 | 25 | ``` 26 | $ npm i -g @vue-cli@next 27 | $ vue create --default multi-page-demo 28 | ``` 29 | 30 | 此时的目录结构为: 31 | 32 | ``` 33 | ... 34 | ├── public 35 | │   ├── favicon.ico 36 | │   └── index.html 37 | ├── src 38 | │   ├── App.vue 39 | │   ├── assets 40 | │   │   └── logo.png 41 | │   ├── components 42 | │   │   └── HelloWorld.vue 43 | │   └── main.js 44 | ``` 45 | 46 | 我们首先需要重构工程目录,可以基于以下或自身的需求进行考虑: 47 | 48 | 1. 通过一个pages目录来存放不同的入口,每个入口包含:index.html、main.js,pages,其中文件夹名即路由名 49 | 2. src目录下可以存放项目的通用组件和静态资源,每个入口单独管理自己独有的资源 50 | 51 | 通过以上考虑,决定将工程结果重构为: 52 | 53 | ``` 54 | ... 55 | ├── public 56 | │   └── favicon.ico 57 | ├── src 58 | │   ├── assets 59 | │   ├── components 60 | │   └── pages 61 | │   └── page1 62 | │   ├── App.vue 63 | │   ├── assets 64 | │   │   └── logo.png 65 | │   ├── components 66 | │   │   └── HelloWorld.vue 67 | │   ├── index.html 68 | │   └── main.js 69 | │   └── page2 70 | │   ├── ... 71 | ``` 72 | 73 | 更改完结构后,我们再通过vue.config.js对入口进行一下调整,保证项目正常运行: 74 | 75 | ``` 76 | // vue.config.js 77 | const path = require("path"); 78 | 79 | module.exports = { 80 | chainWebpack: config => { 81 | config.plugin("html").tap(args => { 82 | args[0].template = path.join(__dirname, "./src/pages/page1/index.html"); 83 | return args; 84 | }); 85 | }, 86 | configureWebpack: { 87 | entry: { 88 | app: path.join(__dirname, "./src/pages/page1/main.js") 89 | } 90 | } 91 | }; 92 | 93 | ``` 94 | 95 | vue.config.js是一个可选文件,用户需要自行创建,它会被`@vue/cli-service`读取。当正确添加配置后,重启一下项目,测试一下项目在改变目录结构后能否正常运行。试想一下,若照着这个思路进行配置多入口,那么首先需要删除或修改掉原有webpack配置项,然后还需添加多入口的一些插件,虽然通过脚手架对外提供的API可以实现,可是这种修改方式还不是直接修改原生构建配置更快,那么还有其他解决方法吗? 96 | 97 | ## Multi-Page模式 98 | 99 | Vue CLI 支持多入口模式,只需要在vue.config.js中提供pages选项即可开启[多入口模式](https://cli.vuejs.org/zh/config/#pages),我们现在将使用pages字段来重构vue.config.js: 100 | 101 | ``` 102 | module.exports = { 103 | pages: { 104 | index: { 105 | // page 的入口 106 | entry: "src/pages/page1/main.js", 107 | // 模板来源 108 | template: "src/pages/page1/index.html", 109 | // 在 dist/index.html 的输出 110 | filename: "index.html", 111 | // 当使用 title 选项时, 112 | // template 中的 title 标签需要是 <%= htmlWebpackPlugin.options.title %> 113 | title: "Index Page", 114 | // 在这个页面中包含的块,默认情况下会包含 115 | // 提取出来的通用 chunk 和 vendor chunk。 116 | chunks: ["chunk-vendors", "chunk-common", "index"] 117 | } 118 | } 119 | }; 120 | ``` 121 | 122 | 配置完成后,服务依然正确访问,至此已经完成了添加多入口的基本准备工作,借助于脚手架多入口构建模式,因此需要做的工作仅剩下一个:**通过目录结构生成对应的入口配置**,也就是说,我们需要实现一个函数,函数的具体功能为: 123 | 124 | ![](http://7xp5r4.com1.z0.glb.clouddn.com/18-8-14/41944374.jpg) 125 | 126 | 以下是具体配置: 127 | 128 | ``` 129 | // vue.config.js 130 | const path = require("path"); 131 | const glob = require("glob"); 132 | const fs = require("fs"); 133 | 134 | const config = { 135 | entry: "main.js", 136 | html: "index.html", 137 | pattern: ["src/pages/*"] 138 | }; 139 | 140 | const genPages = () => { 141 | const pages = {}; 142 | const pageEntries = config.pattern.map(e => { 143 | const matches = glob.sync(path.resolve(__dirname, e)); 144 | return matches.filter( 145 | match => 146 | fs.existsSync(`${match}/${config.entry}`) && 147 | fs.existsSync(`${match}/${config.html}`) 148 | ); 149 | }); 150 | Array.prototype.concat.apply([], pageEntries).forEach(dir => { 151 | const filename = dir.split(path.sep).pop(); 152 | pages[filename] = { 153 | entry: `${dir}/${config.entry}`, 154 | template: `${dir}/${config.html}`, 155 | filename: filename === 'index' ? config.html : `${filename}/${config.html}` 156 | }; 157 | }); 158 | return pages; 159 | }; 160 | 161 | module.exports = { 162 | pages: genPages() 163 | }; 164 | 165 | ``` 166 | 167 | 通过以上配置后,如果是page1的入口,则会最终在dist目录生成一个:`page1/index.html`,因此还需要设置vue.config.js中的devServer.historyApiFallback,确保任意的 404 响应都可能需要被替代为 index.html正常返回: 168 | 169 | ``` 170 | ... 171 | devServer: { 172 | historyApiFallback: true, 173 | }, 174 | ... 175 | ``` 176 | 177 | 至此,我们配置工作就结束了。复制一份page1文件夹为page2,通过将App.vue内的img标签去除,然后重新启动项目,在浏览器中测试一下访问,若以上步骤没问题,那么访问/page2时,应该会出现不带logo的页面,运行`yarn build输出结构如下: 178 | 179 | ``` 180 | . 181 | ├── css 182 | │   ├── page1.5f9ed80b.css 183 | │   └── page2.80dc2e75.css 184 | ├── favicon.ico 185 | ├── img 186 | │   └── logo.82b9c7a5.png 187 | ├── js 188 | │   ├── chunk-vendors.f061f10e.js 189 | │   ├── chunk-vendors.f061f10e.js.map 190 | │   ├── page1.973fb73f.js 191 | │   ├── page1.973fb73f.js.map 192 | │   ├── page2.5e495ede.js 193 | │   └── page2.5e495ede.js.map 194 | ├── page1 195 | │   └── index.html 196 | └── page2 197 | └── index.html 198 | ``` 199 | 200 | ## 源码部分 201 | 202 | @vue/cli-service通过判断是否传入pages参数来生成对应Webpack配置文件,让我们先来看看没有传入时的处理函数: 203 | 204 | ``` 205 | if (!multiPageConfig) { 206 | // default, single page setup. 207 | htmlOptions.template = fs.existsSync(htmlPath) 208 | ? htmlPath 209 | : defaultHtmlPath 210 | 211 | webpackConfig 212 | .plugin('html') 213 | .use(HTMLPlugin, [htmlOptions]) 214 | 215 | if (!isLegacyBundle) { 216 | // inject preload/prefetch to HTML 217 | ... 218 | } 219 | } 220 | ``` 221 | 222 | 由源码可知,pages参数可用于生成三个插件:preload-plugin、prefetch-plugin、html-plugin,若不传html文件则会使用一个只空的默认html文件,而在多入口模式下,代码的逻辑也很简单,在此就不贴源码了,它会执行以下步骤: 223 | 224 | 1. 清除原有entry 225 | 2. 对pages字段的每个key做循环,解析每个入口对象的参数entry(必填)、title、template、filename、chunks 226 | 3. 通过entry字段生成webpack的entry入口 227 | 4. 通过其余参数生成对应的html-webpack-plugin,若不为传统模式,也会生成对应入口的preload插件与prefetch插件 228 | 229 | ## 局部优化 230 | 231 | #### 移除prefetch 232 | 233 | 由于本人并不喜欢为将来做打算,因此并不希望预加载一些可能会用到的asyncChunk,因为会浪费掉一些带宽,而且在多页面中并不见得预加载其他入口的文件是一件好事情,于是我们通过chainWebpack进行删除: 234 | 235 | ``` 236 | modules.exports = { 237 | // ... 238 | chainWebpack: config => { 239 | Object.keys(pages).forEach(entryName => { 240 | config.plugins.delete(`prefetch-${entryName}`); 241 | }); 242 | } 243 | } 244 | ``` 245 | 246 | #### 关闭SourceMap 247 | 248 | 关闭之后不仅能加快生产环境的打包速度,也能避免源码暴露在浏览器端: 249 | 250 | ``` 251 | modules.exports = { 252 | // ... 253 | productionSourceMap: false, 254 | } 255 | ``` 256 | 257 | #### 打包分类(强迫症患者福音) 258 | 259 | 首先回顾一下dist中的部分文件夹: 260 | 261 | ``` 262 | . 263 | ├── css 264 | │   ├── page1.5f9ed80b.css 265 | │   └── page2.80dc2e75.css 266 | ├── js 267 | │   ├── chunk-vendors.f061f10e.js 268 | │   ├── chunk-vendors.f061f10e.js.map 269 | │   ├── page1.973fb73f.js 270 | │   ├── page1.973fb73f.js.map 271 | │   ├── page2.5e495ede.js 272 | │   └── page2.5e495ede.js.map 273 | ├── page1 274 | │   └── index.html 275 | └── page2 276 | └── index.html 277 | ``` 278 | 279 | 其实我们更希望的是不同入口的css与js文件放入不同入口中,而不是统一放在一个js和css文件夹,为了做到这一点,js打包路径我们可已通过修改webpack的output配置来完成,而css打包路径,脚手架是通过MiniCssExtractPlugin插件来完成的,因此可以使用chainWebpack的tap来修改其配置,以上只需要在生产环境修改即可: 280 | 281 | ``` 282 | modules.exports = { 283 | // ... 284 | chainWebpack: config => { 285 | // ... 286 | if (process.env.NODE_ENV === "production") { 287 | config.plugin("extract-css").tap(() => (() => [ 288 | { 289 | filename: "[name]/css/[name].[contenthash:8].css", 290 | chunkFilename: "[name]/css/[name].[contenthash:8].css" 291 | } 292 | ])); 293 | } 294 | }, 295 | configureWebpack: config => { 296 | if (process.env.NODE_ENV === "production") { 297 | config.output = { 298 | path: path.join(__dirname, "./dist"), 299 | filename: "[name]/js/[name].[contenthash:8].js", 300 | publicPath: "/", 301 | chunkFilename: "[name]/js/[name].[contenthash:8].js" 302 | }; 303 | } 304 | } 305 | } 306 | ``` 307 | 308 | 此时打包后的dist文件夹为: 309 | 310 | ``` 311 | ├── page1 312 | │   ├── css 313 | │   │   └── page1.42195c95.css 314 | │   ├── index.html 315 | │   └── js 316 | │   └── page1.569bf4e5.js 317 | └── page2 318 | ├── css 319 | │   └── page2.4e7ad924.css 320 | ├── index.html 321 | └── js 322 | └── page2.05e51252.js 323 | ``` 324 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-cli-multipage", 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-0" 22 | }, 23 | "eslintConfig": { 24 | "root": true, 25 | "env": { 26 | "node": true 27 | }, 28 | "extends": [ 29 | "plugin:vue/vue3-essential", 30 | "eslint:recommended" 31 | ], 32 | "parserOptions": { 33 | "parser": "babel-eslint" 34 | }, 35 | "rules": {} 36 | }, 37 | "browserslist": [ 38 | "> 1%", 39 | "last 2 versions", 40 | "not dead" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vv13/vue-cli-multipage/cc38821876072d402b717b2be694999837cddf71/public/favicon.ico -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vv13/vue-cli-multipage/cc38821876072d402b717b2be694999837cddf71/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vv13/vue-cli-multipage/cc38821876072d402b717b2be694999837cddf71/src/components/.gitkeep -------------------------------------------------------------------------------- /src/pages/index/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/pages/index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-cli-multipage 9 | 10 | 11 |
12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/pages/index/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /src/pages/page2/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/pages/page2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-cli-multipage 9 | 10 | 11 |
12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/pages/page2/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /src/pages/test/page/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /src/pages/test/page/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-cli-multipage 9 | 10 | 11 |
12 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/pages/test/page/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | createApp(App).mount('#app') 5 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const config = { 5 | entry: 'main.js', 6 | html: 'index.html', 7 | pagesRoot: path.resolve(__dirname, 'src/pages') 8 | }; 9 | 10 | const genRoutes = () => { 11 | const allRoutes = []; 12 | 13 | const findAllRoutes = (source, routes) => { 14 | const files = fs.readdirSync(source); 15 | files.forEach(filename => { 16 | const fullname = path.join(source, filename); 17 | const stats = fs.statSync(fullname); 18 | if (!stats.isDirectory()) return; 19 | if (fs.existsSync(`${fullname}/${config.html}`)) { 20 | routes.push(fullname); 21 | } else { 22 | findAllRoutes(fullname, routes); 23 | } 24 | }); 25 | }; 26 | findAllRoutes(config.pagesRoot, allRoutes); 27 | return allRoutes; 28 | }; 29 | 30 | const genPages = () => { 31 | const pages = {}; 32 | genRoutes().forEach(route => { 33 | const filename = route.slice(config.pagesRoot.length + 1); 34 | pages[filename] = { 35 | entry: `${route}/${config.entry}`, 36 | template: `${route}/${config.html}`, 37 | filename: 38 | filename === 'index' ? config.html : `${filename}/${config.html}` 39 | }; 40 | }); 41 | return pages; 42 | }; 43 | 44 | const pages = genPages(); 45 | 46 | module.exports = { 47 | productionSourceMap: false, 48 | pages, 49 | chainWebpack: config => { 50 | // remove prefetch, use import(/* webpackPrefetch: true */ './someAsyncComponent.vue') 51 | Object.keys(pages).forEach(entryName => { 52 | config.plugins.delete(`prefetch-${entryName}`); 53 | }); 54 | if (process.env.NODE_ENV === 'production') { 55 | config.plugin('extract-css').tap(() => [ 56 | { 57 | filename: '[name]/css/[name].[contenthash:8].css', 58 | chunkFilename: '[name]/css/[name].[contenthash:8].css' 59 | } 60 | ]); 61 | } 62 | }, 63 | configureWebpack: config => { 64 | if (process.env.NODE_ENV === 'production') { 65 | config.output = { 66 | path: path.join(__dirname, './dist'), 67 | filename: '[name]/js/[name].[contenthash:8].js', 68 | publicPath: '/', 69 | chunkFilename: '[name]/js/[name].[contenthash:8].js' 70 | }; 71 | } 72 | } 73 | }; 74 | --------------------------------------------------------------------------------