前言

之前一直用 vue 开发单页面应用,最后打包生成的只有一个 .html 和 几个 js 文件,然后把生成的 js 文件都嵌进这个页面。但是在实习的时候遇到的项目是用 vue 开发多页面应用,这让我措手不及,还是要记录一下。

多页面 VS 单页面

多页面

优点:首屏渲染快,SEO 效果好。

为什么多页应用的首屏时间快?
首屏时间叫做页面首个屏幕的内容展现的时间,当我们访问页面的时候,服务器返回一个 html,页面就会展示出来,这个过程只经历了一个 http 请求,所以页面展示的速度非常快。
为什么搜索引擎优化效果好(SEO)?
搜索引擎在做网页排名的时候,要根据网页内容才能给网页权重,来进行网页的排名。搜索引擎是可以识别 html 内容的,而我们每个页面所有的内容都放在 html 中,所以这种多页应用,SEO 排名效果好。

缺点:路由跳转慢

因为每次跳转都需要发出一个 http 请求,如果网络比较慢,在页面之间来回跳转时,就会发现明显的卡顿。

单页面

优点:路由切换快

第一次进入页面的时候会请求一个 html 文件和其他 js 文件。切换路由,其实就是切换到其他组件,此时路径也相应变化,但是并没有新的html文件请求,页面内容也变化了。
原理是:js 会感知到 url 的变化,通过这一点,可以用 js 动态的将当前页面的内容清除掉,然后将下一个页面的内容挂载到当前页面上,这个时候的路由不是后端来做了,而是前端来做,判断页面到底是显示哪个组件,清除不需要的,显示需要的组件。这种过程就是单页应用,每次跳转的时候不需要再请求 html 文件了,这样就节约了很多http发送时延。

缺点:首屏时间慢,SEO差

单页应用的首屏时间慢,首屏时需要请求一次 html ,同时还要发送一次 js 请求,两次请求回来了,首屏才会展示出来。相对于多页应用,首屏时间慢。
SEO 效果差,因为搜索引擎只认识 html 里的内容,不认识 js 的内容,而单页应用的内容都是靠 js 渲染生成出来的,搜索引擎不识别这部分内容,也就不会给一个好的排名,会导致单页应用做出来的网页在百度和谷歌上的排名差。

如何选择

选择单页面还是多页面主要看场景
1.大部分后台系统,ToB 系统,等对数据操作,页面切换频繁,比较注重的是操作的流畅性,spa来做当然会更合适。

  1. 很多ToC 项目,追求的是首屏的展现速度,注重的是观看体验,操作较少,就比较适合多页面,配合服务端渲染。
    但是现在单页面应用框架不断发展,已经有很多解决方案了,比如 SEO, vue 有 nuxt ,react 有 next。首屏展现速度,可以用路由懒加载。相比之下,多页面应用却不能解决每次跳转路由重新下载资源的问题,所以现在单页面应用是一个趋势,但是为啥还要讲 webpack 的多页面配置,(因为还要维护旧项目啊)不同场景下有不同的解决方案。

    webpack 多页面配置

    目录

    进入正题,首先,我们要把目录搞好

关注 src 目录,因为我们是多页面,所以 moduleAmoduleB 是一个模块,insideAinsideB 就是我们实际的页面,外层有一个大的 module 包住 moduleAmoduleB,之后会解释。
可以看到,一个页面,即 insideA 这个文件夹,至少需要三个文件,分别是 app.vueindex.htmlindex.js,内容其实很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
// app.vue
<template>
<div>this is from moduleA</div>
</template>

<script>
export default {

}
</script>

<style>
</style>

1
2
3
4
5
6
7
8
9
10
11
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>moduleA</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
1
2
3
4
5
6
7
8
9
// index.js
import Vue from 'vue';
import App from './app';

new Vue({
el: '#app',
template: '<App/>',
components: { App },
});

因为每次路由跳转都是一个新的页面,所以每个页面都需要一个 vue 实例,如果需要自己写多个 vue 文件,只需要想写单页面文件那样,把写好的 vue 文件 以组件的形式引入到 app.vue 文件里面就能正常使用了。

webpack entry

因为我们是多页面,所以 webpack.base.conf.js 里面的 entry 需要改一下:

1
2
3
4
5
6
7
8
9
// webpack.base.conf.js
module.exports = {
...
// entry: {
// app: './src/main.js'
// },
entry: entries,
...
}

这个 entries 就是我们的目录,这个时候我们就需要一个方法 getEntry 来获取所有打包的入口,我们在 utils 文件里面写这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// utils.js
// Get entry list from global path
const glob = require('glob');
exports.getEntry = function (globPath) {
const entries = {};

glob.sync(globPath).forEach(function (entry) {
const tmp = entry.split('/').splice(-3);
const pathname = `${tmp.splice(0, 1)}/${tmp.splice(0, 1)}`;
entries[pathname] = entry;
console.log(chalk.green('module: ', pathname));
console.log(chalk.cyan('template: ', entry));
});
return entries;
};

然后我们这么用:

1
const entries = utils.getEntry('./src/'+config.dev.tag+'/**/*.js');

最后得到的结果:

1
2
{ 'moduleA/insideA': './src/module/moduleA/insideA/index.js',
'moduleB/insideB': './src/module/moduleB/insideB/index.js' }

注意到 config.dev.tag,上面说过为什么要用 module 包含 moduleAmoduleB,因为我们在开发的时候,有时只改动一个文件,却要编译整个项目,而且整个项目编译一次要等好久,拉低开发效率,所以我们有一个 tag ,只编译 tag 目录下的文件,这个时候只会把 tag 下的文件当成 entry,减少编译的文件,加快我们的开发。设置这个 tag 也很简单,在 config/index.js

1
2
3
4
5
6
7
8
// index.js
module.exports = {
dev: {
...
tag: 'module',
...
}
}

这个时候就只会打包 module 目录下的文件了。

HtmlWebpackPlugin

要知道,多页面应用最特别的就是,每个页面有自己的 js 和 公共的 js,彼此能访问到公共的 js,但是访问不了别人特有的 js,用 webpack 的 HtmlWebpackPlugin 就能帮我们解决这一问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// webpack.dev.conf.js
const pages = utils.getEntry('./src/'+config.dev.tag+'/**/*.html');
Object.keys(pages).forEach((pathname) => {
const conf = {
filename: `${pathname}.html`,
template: pages[pathname],
minify: {
removeComments: true,
},
inject: 'body',
cache: true,
chunks: [pathname],
};
devWebpackConfig.plugins.push(new HtmlWebpackPlugin(conf));
});

循环文件,设置好之后再 pushdevWebpackConfigplugins 里面,chunks 里面就是插进 html 里面的 js,可以把公共的 js 放到里面。

dev-server

其实就是开启服务器调试环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
require('./check-versions')();

const config = require('../config');

if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV);
}

const opn = require('opn');
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const proxyMiddleware = require('http-proxy-middleware');
const webpackConfig = process.env.NODE_ENV === 'testing'
? require('./webpack.prod.conf')
: require('./webpack.dev.conf');
console.log("process.env.NODE_ENV",process.env.NODE_ENV);
// default port where dev server listens for incoming traffic
const port = process.env.PORT || config.dev.port;
// automatically open browser, if not set will be false
const autoOpenBrowser = config.dev.autoOpenBrowser;
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
//const proxyTable = config.dev.proxyTable;

const app = express();
const compiler = webpack(webpackConfig);

const devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: false,
});

const hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {
},
});
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' });
cb();
});
});

// proxy api requests
// Object.keys(proxyTable).forEach(function (context) {
// let options = proxyTable[context];
// if (typeof options === 'string') {
// options = { target: options };
// }
// app.use(proxyMiddleware(options.filter || context, options));
// });

// handle fallback for HTML5 history API
const connectHistoryApiFallbackConf = {
// logger: console.log.bind(console),
// verbose: true,
rewrites: [{
from: /^\/\w+\/\w+$/,
to: function(context) {
return context.parsedUrl.pathname + ".html";
}
}]
}
app.use(require('connect-history-api-fallback')(connectHistoryApiFallbackConf));

// serve webpack bundle output
app.use(devMiddleware);

// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware);

// serve pure static assets
const staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory);
app.use(staticPath, express.static('./statics'));

// const uri = `http://localhost:${port}`;
const uri = `http://0.0.0.0:${port}`;

devMiddleware.waitUntilValid(function () {
console.log(`> Listening at ${uri}\n`);
});

module.exports = app.listen(port, function (err) {
if (err) {
console.log(err);
return;
}

// when env is testing, don't need open it
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri);
}
});

在 webpack-dev-server 里使用路由,能够实现访问 /a 时候显示 a.html,/b 显示 b.html。

package.json

就是把 script 里面的 npm run dev 改成 node --optimize_for_size build/dev-server.js 启动我们的 webpack 服务器。

效果

参考
使用webpack构建多页面应用
单页应用和多页应用
webpack搭建多页面系统(三) 理解webpack.config.js的四个核心概念