上篇文章中我们成功打包并输出了多页文件,而构建一个多页应用能够让我们进一步了解项目配置的可拓展性,可以对学习 Vue 和 webpack 起到强化训练的效果,本文将在此基础上主要针对多页路由及模板的配置进行系列的介绍。
路由配置
1. 跳转
在配置路由前,首先我们要明确一点就是,多页应用中的每个单页都是相互隔离的,即如果你想从 page1 下的路由跳到 page2 下的路由,你无法使用 vue-router 中的方法进行跳转,需要使用原生方法:location.href
或 location.replace
。
此外为了能够清晰的分辨路由属于哪个单页,我们应该给每个单页路由添加前缀,比如:
-
index 单页:/vue/
-
page1 单页:/vue/page1/
-
page2 单页:/vue/page2/
其中 /vue/ 为项目的二级目录,其后的目录代表路由属于哪个单页。因此我们每个单页的路由配置可以像这样:
/* page1 单页路由配置 */
import Vue from 'vue'
import Router from 'vue-router'
// 首页
const Home = (resolve => {
//使用了 require.ensure 方法来实现懒加载
require.ensure(['../views/home.vue'], () => {
resolve(require('../views/home.vue'))
})
})
Vue.use(Router)
//通过 process.env.BASE_URL 动态获取基础 URL,并添加 'page1' 作为前缀
let base = `${process.env.BASE_URL}` + 'page1'; // 添加单页前缀
export default new Router({
mode: 'history',
base: base,
routes: [
{
path: '/',
name: 'home',
//动态加载 Home 组件
component: Home
},
]
})
我们通过设置路由的 base 值来为每个单页添加路由前缀,如果是 index 单页我们无需拼接路由前缀,直接跳转至二级目录即可。
那么在单页间跳转的地方,我们可以这样写:
<template>
<div id="app">
<div id="nav">
<a @click="goFn('')">Index</a> |
<a @click="goFn('page1')">Page1</a> |
<a @click="goFn('page2')">Page2</a> |
</div>
<router-view/>
</div>
</template>
<script>
export default {
methods: {
goFn(name) {
location.href = `${process.env.BASE_URL}` + name
}
}
}
</script>
用
location.href
进行导航会导致页面完全刷新,从而失去单页应用(SPA)的优点。为了保持 SPA 的特性,可以使用 Vue Router 提供的$router.push
方法进行路由导航,export default {
methods: {
goFn(name) {
this.$router.push(name); // 使用 Vue Router 进行路由导航
}
}
}
但是为了保持和 Vue 路由跳转同样的风格,我可以对单页之间的跳转做一下封装,实现一个 Navigator
类,类的代码可以查看本文最后的示例,封装完成后我们可以将跳转方法修改为:
this.$openRouter({
name: name, // 跳转地址
query: {
text: 'hello' // 可以进行参数传递
},
})
使用上述 this.$openRouter() 来调用 Navigator.openRouter
方法我们还需要一个前提条件,便是通过 Vue.prototype
将其绑定到 Vue 的原型链上,我们在所有单页的入口文件中添加:
//引入navigator对象
import { Navigator } from '../../common' // 引入 Navigator
// 添加至 Vue 原型链
Vue.prototype.$openRouter = Navigator.openRouter;
Vue 3 示例
import { createApp } from 'vue';
import App from './App.vue';
import { Navigator } from '../../common';
const app = createApp(App);
// 添加到全局属性
app.config.globalProperties.$openRouter = Navigator.openRouter;
app.mount('#app');
至此我们已经能够成功模仿 vue-router 进行单页间的跳转,但是需要注意的是因为其本质使用的是 location 跳转,所以必然会产生浏览器的刷新与重载。
2. 重定向
当我们完成上述路由跳转的功能后,可以在本地服务器上来进行一下测试,你会发现 Index 首页可以正常打开,但是跳转 Page1、Page2 却仍然处于 Index 父组件下,这是因为浏览器认为你所要跳转的页面还是在 Index 根路由下,同时又没有匹配到 Index 单页中对应的路由。这时候我们服务器需要做一次重定向,将下方路由指向对应的 html 文件即可:
/vue/page1 -> /vue/page1.html
/vue/page2 -> /vue/page2.html
在 vue.config.js 中,我们需要对 devServer 进行配置,添加 historyApiFallback
配置项,该配置项主要用于解决 HTML5 History API 产生的问题,比如其 rewrites 选项用于重写路由:
/* vue.config.js */
let baseUrl = '/vue/';
module.exports = {
...
devServer: {
historyApiFallback: {
rewrites: [
{ from: new RegExp(baseUrl + 'page1'), to: baseUrl + 'page1.html' },
{ from: new RegExp(baseUrl + 'page2'), to: baseUrl + 'page2.html' },
]
}
}
...
}
上方我们通过 rewrites 匹配正则表达式的方式将 /vue/page1
这样的路由替换为访问服务器下正确 html 文件的形式。当请求的路径符合 from
中的正则表达式时,会被重定向到 to
指定的 HTML 文件,例如,如果用户请求 /vue/page1
,则开发服务器会返回 /vue/page1.html
文件的内容。如此不同单页间便可以进行正确跳转和访问了。最后需要注意的是如果你的应用发布到正式服务器上,你同样需要让服务器或者中间层作出合理解析。
参考:HTML5 History 模式 # 后端配置例子
而更多关于 historyApiFallback 的信息可以访问:connect-history-api-fallback
拓展1
new RegExp的简易概括
new RegExp
是 JavaScript 中用于创建正则表达式对象的构造函数。正则表达式是一种强大的文本处理工具,可以用于模式匹配和搜索。通过 new RegExp
,你可以动态构造正则表达式,而不仅限于字面量表示法(使用斜杠 /
包围的形式)。
基本语法
let regex = new RegExp(pattern, flags);
- pattern: 一个字符串,定义了正则表达式的模式。
- flags: 可选字符串,定义了正则表达式的修饰符(例如,
g
表示全局匹配,i
表示不区分大小写,m
表示多行匹配等)。
示例
基本示例:
let pattern = 'abc';
let regex = new RegExp(pattern);
console.log(regex.test('abcdef')); // true
console.log(regex.test('xyz')); // false
使用修饰符:
let pattern = 'abc';
let regex = new RegExp(pattern, 'i'); // 'i' 表示不区分大小写
console.log(regex.test('ABCdef')); // true
动态构造正则表达式
使用 new RegExp
可以根据变量动态构造正则表达式:
let searchTerm = 'page';
let regex = new RegExp(searchTerm);
console.log(regex.test('This is a page.')); // true
在 Vue.js 配置中的使用
在之前的 vue.config.js
中,使用 new RegExp
创建动态正则表达式,用于匹配特定的 URL 路径。例如:
rewrites: [
{ from: new RegExp(baseUrl + 'page1'), to: baseUrl + 'page1.html' },
{ from: new RegExp(baseUrl + 'page2'), to: baseUrl + 'page2.html' },
]
new RegExp(baseUrl + 'page1')
创建了一个正则表达式,用于匹配以 /vue/page1
开头的 URL。当用户直接访问这个 URL 时,开发服务器会将请求重定向到 page1.html
。
总结
- 使用
new RegExp
可以动态创建正则表达式,适用于需要根据变量或用户输入构造正则的场景。 - 正则表达式在很多情况下非常有用,尤其是在处理文本、匹配模式和验证输入等方面。
- 在 Vue.js 或其他框架的配置中,可以利用正则表达式进行复杂的路由匹配和 URL 重写。
模板配置
上篇文章我们已经介绍了关于多模板的读取和配置,在配置 html-webpack-plugin 的时候我们提到了自定义配置,这里我将结合模板渲染的功能来进行统一介绍。
1. 模板渲染
这里所说的模板渲染是在我们的 html 模板文件中使用 html-webpack-plugin 提供的 default template 语法进行模板编写,比如:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>模板</title>
<% for (var chunk in htmlWebpackPlugin.files.css) { %>
<% if(htmlWebpackPlugin.files.css[chunk]) {%>
<link href="<%= htmlWebpackPlugin.files.css[chunk] %>" rel="stylesheet" />
<%}%>
<% } %>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<% for (var chunk in htmlWebpackPlugin.files.js) { %>
<% if(htmlWebpackPlugin.files.js[chunk]) {%>
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.js[chunk] %>"></script>
<%}%>
<% } %>
</body>
</html>
-
文档类型声明和头部:
<!DOCTYPE html>
: 声明 HTML5 文档类型。<head>
部分包含页面的元数据,如字符集、视口设置和标题。
-
动态注入 CSS:
- 使用 EJS 模板语法 (
<% ... %>
) 来循环遍历htmlWebpackPlugin.files.css
中的 CSS 文件。 - 如果文件存在,则生成对应的
<link>
标签,将 CSS 文件链接到页面中。
- 使用 EJS 模板语法 (
-
主体内容:
<body>
部分包含一个<div id="app"></div>
,这是 Vue.js 或其他前端框架通常用于挂载应用的根元素。
-
动态注入 JavaScript:
- 类似于 CSS 的处理,使用 EJS 模板语法来遍历
htmlWebpackPlugin.files.js
中的 JavaScript 文件。 - 如果文件存在,则生成对应的
<script>
标签,将 JS 文件链接到页面中。
- 类似于 CSS 的处理,使用 EJS 模板语法来遍历
以上我们使用模板语法手动获取并遍历 htmlWebpackPlugin 打包后的文件并生成到模板中,其中的 htmlWebpackPlugin
变量是模板提供的可访问变量,其有以下特定数据:
"htmlWebpackPlugin": {
"files": {
"css": [ "main.css" ],
"js": [ "assets/head_bundle.js", "assets/main_bundle.js"],
"chunks": {
"head": {
"entry": "assets/head_bundle.js",
"css": [ "main.css" ]
},
"main": {
"entry": "assets/main_bundle.js",
"css": []
},
}
}
}
我们通过 htmlWebpackPlugin.files
可以获取打包输出的 js 及 css 文件路径,包括入口文件路径等。
结合 html 模板文件和的 htmlWebpackPlugin
配置,最终生成的 HTML 文件会像下面这样:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>模板</title>
<!-- 注入的 CSS -->
<link href="main.css" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<!-- 注入的 JavaScript -->
<script type="text/javascript" src="assets/head_bundle.js"></script>
<script type="text/javascript" src="assets/main_bundle.js"></script>
</body>
</html>
需要注意的是如果你在模板中编写了插入对应 js 及 css 的语法,你需要设置 inject
的值为 false 来关闭资源的自动注入:
/* utils.js */
...
let conf = {
entry: filePath, // page 的入口
template: filePath, // 模板路径
filename: filename + '.html', // 生成 html 的文件名
chunks: ['manifest', 'vendor', filename],
inject: false, // 关闭资源自动注入
}
...
否则在页面会引入两次资源,如下图所示:
2. 自定义配置
在模板渲染中,我们只能够使用 htmlWebpackPlugin 内部的一些属性和方法来进行模板的定制化开发,那么如果遇到需要根据不同环境来引入不同资源,同时不同模板间的配置还可能不一样的需求情况的话,我们使用自定义配置会比较方便。比如我们需要在生产环境模板中引入第三方统计脚本:
/* vue.config.js */
module.exports = {
...
pages: utils.setPages({
addScript() {
if (process.env.NODE_ENV === 'production') {
return `
<script src="https://s95.cnzz.com/z_stat.php?id=xxx&web_id=xxx" language="JavaScript"></script>
`
}
return ''
}
}),
...
}
然后在页面模板中通过 htmlWebpackPlugin.options
获取自定义配置对象并进行输出:
<% if(htmlWebpackPlugin.options.addScript){ %>
<%= htmlWebpackPlugin.options.addScript() %>
<%}%>
同时你也可以针对个别模板进行配置,比如我想只在 Index 单页中添加统计脚本,在 Page1 单页中添加其他脚本,那么你可以给 addScript 传入标识符来进行判断输出,比如:
<% if(htmlWebpackPlugin.options.addScript){ %>
<%= htmlWebpackPlugin.options.addScript('index') %>
<%}%>
同时为 addScript 方法添加参数 from:
addScript(from) {
if (process.env.NODE_ENV === 'production') {
let url = "https://xxx";
if (from === 'index') {
url = "https://s95.cnzz.com/z_stat.php?id=xxx&web_id=xxx";
}
return `
<script src=${url} language="JavaScript"></script>
`
}
return ''
}
这样我们就完成了自定义配置中的模板渲染功能。当然根据实际项目需求你的自定义配置项可能会更加复杂和灵活。
拓展2
1、多页应用中各自的 Vuex Store
信息能实现共享吗
1. 使用 Local Storage 或 Session Storage
通过使用浏览器的 Local Storage 或 Session Storage,可以在不同页面之间共享状态。每个页面在加载时可以从 Local Storage 中读取状态,在状态变化时更新 Local Storage。
// 设置状态到 Local Storage
localStorage.setItem('myStoreState', JSON.stringify(store.state));
// 从 Local Storage 获取状态
const savedState = JSON.parse(localStorage.getItem('myStoreState'));
if (savedState) {
store.replaceState(savedState);
}
这种方法具有一定的局限性,因为它不能实时同步状态,用户在一个页面上进行的修改不会立即反映到其他页面上。
2. 使用 URL 参数
如果状态比较简单,可以通过 URL 参数在页面间传递状态。例如,在一个页面中点击链接,带上状态参数:
<a href="page2.html?myState=value">Go to Page 2</a>
在目标页面中,可以读取 URL 参数来获取状态:
const urlParams = new URLSearchParams(window.location.search);
const myState = urlParams.get('myState');
3. 使用 WebSocket 或其他实时通信技术
如果需要在页面间实现实时的状态共享,可以使用 WebSocket、Server-Sent Events 或其他实时通信技术。每个页面都可以连接到同一个 WebSocket 服务器,以便在状态变化时进行广播。
4. 使用 Shared Worker
Shared Worker 是一种在多个浏览器上下文(如多个标签页或窗口)间共享的 Worker。通过 Shared Worker,你可以在多个页面间共享状态。
// sharedWorker.js
let connections = [];
const storeState = { /* initial state */ };
self.onconnect = function(event) {
const port = event.ports[0];
connections.push(port);
port.onmessage = function(e) {
// 更新状态逻辑
storeState.value = e.data;
connections.forEach(conn => conn.postMessage(storeState));
};
};
然后在每个页面中连接到这个 Shared Worker:
const worker = new SharedWorker('sharedWorker.js');
worker.port.onmessage = function(e) {
// 处理状态更新
const sharedState = e.data;
};
worker.port.postMessage(newState);
5. 使用 Service Workers
Service Workers 可以缓存数据并在多个页面间共享,虽然通常它们用于缓存请求和离线功能,但也可以通过 IndexedDB 或其他方式存储状态。
注意事项
- 复杂性: 实现状态共享可能会增加应用的复杂性,特别是在处理状态同步和冲突时。
- 性能: 考虑性能影响,尤其是当状态较大或更新频繁时,使用 WebSocket 或 Shared Worker 可能更合适。
- 设计模式: 设计时要考虑如何管理状态的生命周期,确保在适当的时间清理不再需要的状态。
2.html-webpack-plugin 如何解析非 .html 的模板,比如 .hbs,应该如何配置?
html-webpack-plugin
是用于生成 HTML 文件的 Webpack 插件,默认情况下,它使用 .html
文件作为模板。但是,如果想使用其他类型的模板文件(如 .hbs
,即 Handlebars 模板),可以通过配置相应的加载器来实现。
以下是如何配置 html-webpack-plugin
以解析 .hbs
模板的步骤:
1. 安装必要的依赖
首先,确保安装了 html-webpack-plugin
和 handlebars-loader
(用于处理 Handlebars 模板):
npm install html-webpack-plugin handlebars-loader --save-dev
2. 配置 Webpack
然后,需要在 Webpack 配置文件中配置这两个包。一个典型的 Webpack 配置可能类似于以下内容:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js', // 你的入口文件
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.hbs$/,
loader: 'handlebars-loader', // 使用 Handlebars 加载器
},
// 其他加载器...
],
},
plugins: [
new HtmlWebpackPlugin({
template: './src/template.hbs', // 指定你的 Handlebars 模板
filename: 'index.html', // 输出的 HTML 文件名
}),
],
};
3. 使用 Handlebars 模板
在 .hbs
模板文件中,可以使用 Handlebars 的语法来定义 HTML 结构。例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
</head>
<body>
<div id="app"></div>
<script src="bundle.js"></script>
</body>
</html>
4. 提供模板数据
如果需要在模板中使用动态数据(如标题等),可以在插件配置中通过 templateParameters
属性提供这些数据:
new HtmlWebpackPlugin({
template: './src/template.hbs',
filename: 'index.html',
templateParameters: {
title: 'My Handlebars Template',
// 其他参数...
},
}),
这样,{{title}}
将会被替换为 'My Handlebars Template'
。