Vue使用pages构建多页应用的实现步骤
作者:Nejosi_念旧
概念
首先我们可以把多页应用理解为由多个单页构成的应用,而何谓多个单页呢?其实你可以把一个单页看成是一个 html 文件,那么多个单页便是多个 html 文件,多页应用便是由多个 html 组成的应用,如下图所示:
既然多页应用拥有多个 html,那么同样其应该拥有多个独立的入口文件、组件、路由、vuex 等。没错,说简单一点就是多页应用的每个单页都可以拥有单页应用 src 目录下的文件及功能,我们来看一下一个基础多页应用的目录结构:
├── node_modules # 项目依赖包目录 ├── build # 项目 webpack 功能目录 ├── config # 项目配置项文件夹 ├── src # 前端资源目录 │ ├── images # 图片目录 │ ├── components # 公共组件目录 │ ├── pages # 页面目录 │ │ ├── page1 # page1 目录 │ │ │ ├── components # page1 组件目录 │ │ │ ├── router # page1 路由目录 │ │ │ ├── views # page1 页面目录 │ │ │ ├── page1.html # page1 html 模板 │ │ │ ├── page1.vue # page1 vue 配置文件 │ │ │ └── page1.js # page1 入口文件 │ │ ├── page2 # page2 目录 │ │ └── index # index 目录 │ ├── common # 公共方法目录 │ └── store # 状态管理 store 目录 ├── .gitignore # git 忽略文件 ├── .env # 全局环境配置文件 ├── .env.dev # 开发环境配置文件 ├── .postcssrc.js # postcss 配置文件 ├── babel.config.js # babel 配置文件 ├── package.json # 包管理文件 ├── vue.config.js # CLI 配置文件 └── yarn.lock # yarn 依赖信息文件
根据上方目录结构我们可以看出其实 pages 下的一个目录就是一个单页包含的功能,这里我们包含了 3 个目录就构成了多页应用。
除了目录结构的不同外,其实区别单页应用,多页应用在很多配置上都需要进行修改,比如单入口变为多入口、单模板变为多模板等,那么下面我们就来了解一下多页应用的具体实现。
多入口
在单页应用中,我们的入口文件只有一个,Vue CLI 默认配置的是 main.js,但是到了多页应用,我们的入口文件便包含了 page1.js、page2.js、index.js等,数量取决于 pages 文件夹下目录的个数,这时候为了项目的可拓展性,我们需要自动计算入口文件的数量并解析路径配置到 webpack 中的 entry 属性上,如:
module.exports = { ... entry: { page1: '/xxx/pages/page1/page1.js', page2: '/xxx/pages/page2/page2.js', index: '/xxx/pages/index/index.js', }, ... }
那么我们如何读取并解析这样的路径呢,这里就需要使用工具和函数来解决了。我们可以在根目录新建 build 文件夹存放 utils.js 这样共用的 webpack 功能性文件,并加入多入口读取解析方法:
/* utils.js */ const path = require('path'); // glob 是 webpack 安装时依赖的一个第三方模块,该模块允许你使用 * 等符号, // 例如 lib/*.js 就是获取 lib 文件夹下的所有 js 后缀名的文件 const glob = require('glob'); // 取得相应的页面路径,因为之前的配置,所以是 src 文件夹下的 pages 文件夹 // PAGE_PATH 变量定义了存放页面文件的路径 const PAGE_PATH = path.resolve(__dirname, '../src/pages'); /* * 多入口配置 * 通过 glob 模块读取 pages 文件夹下的所有对应文件夹下的 js * 后缀文件,如果该文件存在 * 那么就作为入口处理 */ exports.getEntries = () => { //glob.sync 方法同步读取 PAGE_PATH 下的所有子目录中的 .js 文件,将匹配到的文件路径存储在 entryFiles 数组中 let entryFiles = glob.sync(PAGE_PATH + '/*/*.js') let map = {} // 遍历所有入口文件,和构建入口对象 entryFiles.forEach(filePath => { // 获取文件名 let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.')) // 通过 forEach 遍历每个文件路径,提取文件名,并将文件名(不带扩展名)作为键,将文件路径作为值,构建成一个对象 map map[filename] = filePath }) return map }
最终返回的
map
对象包含了所有入口文件的映射关系,形式如下
{ 'page1': '/path/to/src/pages/page1/index.js', 'page2': '/path/to/src/pages/page2/index.js', ... }
在 Webpack 配置中使用 getEntries
获取入口文件,并配置 entry
属性:
const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const { getEntries } = require('./utils'); module.exports = { entry: getEntries(), // 使用 getEntries 方法获取入口 output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash].js', // 依据入口名称生成文件 }, plugins: Object.keys(getEntries()).map(entry => { return new HtmlWebpackPlugin({ template: path.resolve(__dirname, `../src/pages/${entry}/index.html`), // 根据页面生成 HTML filename: `${entry}.html`, chunks: [entry], // 只引入当前入口的 chunk }); }), module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } } ] }, optimization: { splitChunks: { chunks: 'all', }, } };
上方我们使用了 glob 这一第三方模块读取所有 pages 文件夹下的入口文件,其需要进行安装:
yarn add glob --dev
这样我们多入口的设置便完成了,当然这并不是 CLI 所希望的操作,后面我们会进行改进。
多模板
相对于多入口来说,多模板的配置也是大同小异,这里所说的模板便是每个 page 下的 html 模板文件,而模板文件的作用主要用于 webpack 中 html-webpack-plugin
插件的配置,其会根据模板文件生产一个编译后的 html 文件并自动加入携带 hash 的脚本和样式,基本配置如下:
/* webpack 配置文件 */ const HtmlWebpackPlugin = require('html-webpack-plugin') // 安装并引用插件 module.exports = { ... plugins: [ new HtmlWebpackPlugin({ title: 'My Page', // 生成 html 中的 title filename: 'demo.html', // 生成 html 的文件名 template: 'xxx/xxx/demo.html', // 模板路径 chunks: ['manifest', 'vendor', 'demo'], // 所要包含的模块 inject: true, // 是否注入资源 }) ] ... }
以上是单模板的配置,那么如果是多模板只要继续往 plugins 数组中添加 HtmlWebpackPlugin 即可,但是为了和多入口一样能够灵活的获取 pages 目录下所有模板文件并进行配置,我们可以在 utils.js 中添加多模板的读取解析方法:
/* utils.js */ // 多页面输出配置 // 与上面的多页面入口配置相同,读取 page 文件夹下的对应的 html 后缀文件,然后放入数组中 exports.htmlPlugin = configs => { let entryHtml = glob.sync(PAGE_PATH + '/*/*.html') let arr = [] entryHtml.forEach(filePath => { let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.')) let conf = { template: filePath, // 模板路径 filename: filename + '.html', // 生成 html 的文件名 chunks: ['manifest', 'vendor', filename], inject: true, } // 如果有自定义配置可以进行 merge if (configs) { conf = merge(conf, configs) } // 针对生产环境配置 if (process.env.NODE_ENV === 'production') { conf = merge(conf, { minify: { removeComments: true, // 删除 html 中的注释代码 collapseWhitespace: true, // 删除 html 中的空白符 // removeAttributeQuotes: true // 删除 html 元素中属性的引号 }, chunksSortMode: 'manual' // 按 manual 的顺序引入 }) } arr.push(new HtmlWebpackPlugin(conf)) }) return arr }
这里解读一下代码:
导出 htmlPlugin
函数:
exports.htmlPlugin = configs => { let entryHtml = glob.sync(PAGE_PATH + '/*/*.html'); let arr = [];
htmlPlugin
函数接收一个可选的configs
参数,用于自定义配置。- 使用
glob.sync
方法读取PAGE_PATH
下的所有 HTML 文件,并将路径存储在entryHtml
数组中。
遍历 HTML 文件:
entryHtml.forEach(filePath => { let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.'))
- 对每个找到的 HTML 文件进行遍历,提取文件名(去掉路径和扩展名)。
创建配置对象:
let conf = { template: filePath, // 模板路径 filename: filename + '.html', // 生成 HTML 的文件名 chunks: ['manifest', 'vendor', filename], inject: true, };
conf
对象包含了HtmlWebpackPlugin
所需的配置信息:template
: 指定当前 HTML 文件作为模板。filename
: 生成的 HTML 文件的名称。chunks
: 指定需要引入的 JS 文件。inject
: 设置为true
,自动注入 JS 文件。
合并自定义配置:
if (configs) { conf = merge(conf, configs); }
如果提供了自定义配置 configs
,使用 merge
函数合并配置。
生产环境配置:
if (process.env.NODE_ENV === 'production') { conf = merge(conf, { minify: { removeComments: true, // 删除 HTML 中的注释代码 collapseWhitespace: true, // 删除 HTML 中的空白符 }, chunksSortMode: 'manual' // 按 manual 的顺序引入 }); }
- 如果当前环境是生产环境,合并额外的配置以优化输出的 HTML 文件,例如:
minify
: 提供 HTML 最小化的选项。chunksSortMode
: 指定引入 JS 文件的顺序。
推送 HtmlWebpackPlugin
实例到数组:
arr.push(new HtmlWebpackPlugin(conf));
将每个 HtmlWebpackPlugin
实例推入 arr
数组中。
以上我们仍然是使用 glob 读取所有模板文件,然后将其遍历并设置每个模板的 config,同时针对一些自定义配置和生产环境的配置进行了 merge 处理,其中自定义配置的功能我会在下节进行介绍,这里介绍一下生产环境下 minify
配置的作用:将 html-minifier 的选项作为对象来缩小输出。
html-minifier 是一款用于缩小 html 文件大小的工具,其有很多配置项功能,包括上述所列举的常用的删除注释、空白、引号等。
当我们编写完了多模板的方法后,我们同样可以在 vue.config.js 中进行配置,与多入口不同的是我们在 configureWebpack 中不能直接替换 plugins 的值,因为它还包含了其他插件,使用 return 返回一个对象来进行 merge 操作
/* vue.config.js */ const utils = require('./build/utils') module.exports = { ... configureWebpack: config => { config.entry = utils.getEntries() // 直接覆盖 entry 配置 // 使用 return 一个对象会通过 webpack-merge 进行合并,plugins 不会置空 return { plugins: [...utils.htmlPlugin()] } }, ... }
如此我们多页应用的多入口和多模板的配置就完成了,这时候我们运行命令 yarn build
后你会发现 dist 目录下生成了 3 个 html 文件,分别是 index.html、page1.html 和 page2.html。
使用 pages 配置
其实,在 vue.config.js 中,我们还有一个配置没有使用,便是 pages。pages 对象允许我们为应用配置多个入口及模板,这就为我们的多页应用提供了开放的配置入口。官方示例代码如下:
/* vue.config.js */ module.exports = { pages: { index: { entry: 'src/index/main.js', template: 'public/index.html', filename: 'index.html', title: 'Index Page', chunks: ['chunk-vendors', 'chunk-common', 'index'] }, subpage: 'src/subpage/main.js' } }
首页
index
的配置:entry
: 指定页面的入口文件,这里是src/index/main.js
。template
: 指定模板文件的路径,这里是public/index.html
。该模板用于生成最终的 HTML 文件。filename
: 指定生成的 HTML 文件的名称,在构建后会输出到dist/index.html
。title
: 设置页面的标题。在模板文件中,应该使用<title><%= htmlWebpackPlugin.options.title %></title>
来动态设置标题。chunks
: 指定在此页面中包含的块,默认为提取出来的通用 chunk 和 vendor chunk。这里包括了chunk-vendors
、chunk-common
和index
。
子页面
subpage
的配置:subpage
使用字符串格式,只指定了入口文件src/subpage/main.js
。在这种情况下,模板文件会默认为public/subpage.html
,如果不存在,则回退到public/index.html
。输出的文件名会被推导为subpage.html
。
我们不难发现,pages 对象中的 key 就是入口的别名,而其 value 对象其实是入口 entry 和模板属性的合并,这样我们上述介绍的获取多入口和多模板的方法就可以合并成一个函数来进行多页的处理,合并后的 setPages 方法如下:
// pages 多入口配置 exports.setPages = configs => { let entryFiles = glob.sync(PAGE_PATH + '/*/*.js') let map = {} entryFiles.forEach(filePath => { let filename = filePath.substring(filePath.lastIndexOf('\/') + 1, filePath.lastIndexOf('.')) let tmp = filePath.substring(0, filePath.lastIndexOf('\/')) let conf = { // page 的入口 entry: filePath, // 模板来源,这里假设每个 JS 文件对应一个同名的 HTML 文件 template: tmp + '.html', // 在 dist/index.html 的输出HTML 文件 filename: filename + '.html', // 页面模板需要加对应的js脚本,如果不加这行则每个页面都会引入所有的js脚本 chunks: ['manifest', 'vendor', filename], inject: true, }; if (configs) { conf = merge(conf, configs) } if (process.env.NODE_ENV === 'production') { conf = merge(conf, { minify: { removeComments: true, // 删除 html 中的注释代码 collapseWhitespace: true, // 删除 html 中的空白符 // removeAttributeQuotes: true // 删除 html 元素中属性的引号 }, chunksSortMode: 'manual'// 按 manual 的顺序引入 }) } map[filename] = conf }) return map }
上述代码我们 return 出的 map 对象就是 pages 所需要的配置项结构,我们只需在 vue.config.js 中引用即可:
/* vue.config.js */ const utils = require('./build/utils') module.exports = { ... pages: utils.setPages(), ... }
这样我们多页应用基于 pages 配置的改进就大功告成了,当你运行打包命令来查看输出结果的时候,你会发现和之前的方式相比并没有什么变化,这就说明这两种方式都适用于多页的构建,但是这里还是推荐大家使用更便捷的 pages 配置。
拓展
1.多页应用相比单页应用有哪些优点和缺点? 多页面应用(MPA) 优点
SEO友好:
MPA的每个页面都有独立的URL,容易被搜索引擎索引,对于需要良好SEO表现的应用(如电商网站、博客等)尤为重要。
初次加载速度快:
每个页面都是独立的HTML文档,初始请求时只加载当前页面的资源,因而加载速度通常较快。
简单的框架:
代码结构清晰,各页面相对独立,适合于大中型团队的分工开发。每个页面可以使用不同的框架或技术栈。
更好的浏览器兼容性:
MPA通常与传统的网页架构相似,可能对老旧浏览器有更好的支持。
易于管理状态:
由于每个页面是独立加载的,不需要管理复杂的状态(如 SPA 中的全局状态管理),开发相对简单。 缺点
用户体验较差:
每次页面切换都需要重新加载整个页面,造成明显的闪烁和延迟,用户体验相对较差。
资源加载冗余:
各个页面可能会重复加载相同的资源(如 CSS、JS),造成网络资源浪费。
复杂的路由管理:
随着页面数量的增加,管理路由和导航可能会变得复杂。
开发效率低:
需要为每个页面单独进行开发和维护,可能会影响整体开发效率。 单页面应用(SPA) 优点
更快的用户体验:
通过 AJAX 和动态更新页面内容,用户在不同页面间切换时不需要重新加载整个页面,提供更流畅的体验。
资源共享:
所有页面共享相同的资源(如 JS、CSS),减少了加载时间和服务器请求次数。
动态内容更新:
SPA可以通过 API 动态加载数据,实现更丰富的用户交互,而无需刷新页面。
更好的状态管理:
使用状态管理库(如 Vuex、Redux)可以集中管理应用状态,适合复杂的用户交互。
开发效率:
可以在同一个页面中不断更新内容,减少重新加载的需要,提高开发效率。 缺点
SEO挑战:
SPA的内容通常是通过 JavaScript 动态加载,传统搜索引擎对这种内容的索引能力较弱,可能影响SEO。
首次加载速度慢:
首次访问时需要加载所有的 JavaScript 和 CSS 资源,可能造成加载时间较长。
复杂的路由和状态管理:
SPA需要处理复杂的路由和状态管理,开发者需要花费更多时间来设计和实现。
浏览器兼容性:
对于某些老旧的浏览器,SPA可能存在兼容性问题,尤其是与新特性相关的功能。chunksSortMode
除了文中介绍的 manual
手动排序外,还有哪些排序方式? 2.chunksSortMode
除了文中介绍的 manual
手动排序外,还有哪些排序方式?
常见的排序方式
none
:
不对 chunks 进行排序,保持 Webpack 生成 chunk 的原始顺序。
auto
:
自动排序,这通常是基于 chunk 的名称和文件大小进行排序。Webpack 会尝试根据某种逻辑来决定 chunks 的顺序,通常是将较小的 chunk 放在前面。
dependency
:
根据 chunks 之间的依赖关系进行排序,确保在某个 chunk 被使用之前,所有它依赖的 chunk 都已经加载。
size
:
根据 chunk 的大小进行排序,较大的 chunk 会被放在后面,较小的 chunk 会被放在前面。
name
:
根据 chunk 的名称进行字母排序。通常在构建大型应用时,使用名称排序可以更好地控制输出的顺序。manual: 开发者需要自己维护 chunks 的顺序 3.glob 中 *
和 **
的区别是什么 *
(星号) 匹配文件或目录的任意名称: *
可以匹配零个或多个字符,但不包括路径分隔符(如 /
或 \
)。使用场景: 匹配当前目录下的所有文件和目录。匹配特定类型的文件,如 *.js
匹配所有 JavaScript 文件。
示例:
*.js
匹配当前目录下所有以 .js
结尾的文件。dir/*
匹配 dir
目录下的所有文件和目录,但不匹配 dir/subdir
中的内容。 **
(双星号) 匹配任意深度的目录: **
可以匹配零个或多个目录,并且可以跨越多个路径分隔符。使用场景: 当你想要匹配某个目录及其所有子目录中的文件时,使用 **
是非常方便的。
示例:
**/*.js
匹配当前目录及所有子目录下的所有 JavaScript 文件。dir/**
匹配 dir
目录及其所有子目录下的所有文件和目录。 举个例子
假设有以下目录结构:
project/ ├── file1.js ├── file2.txt ├── dir/ │ ├── file3.js │ └── subdir/ │ └── file4.js
使用 *.js
:
匹配结果: file1.js
使用 dir/*
:
匹配结果: dir/file3.js
使用 **/*.js
:
匹配结果: file1.js
, dir/file3.js
, dir/subdir/file4.js
使用 dir/**
:
匹配结果: dir/file3.js
, dir/subdir
, dir/subdir/file4.js
总结
*
用于匹配当前目录下的文件或目录的名称,不跨越目录。**
用于匹配任意深度的目录和子目录,适合需要遍历整个目录树的场景。
以上就是Vue使用pages构建多页应用的实现步骤的详细内容,更多关于Vue pages多页应用的资料请关注脚本之家其它相关文章!