14. 搭建多页应用

我们在第2章中简单介绍过 Webpack 的基本概念,以及 Vue CLI 的基本使用方式。本章会介绍如何使用 Webpack 或者 Vue CLI 搭建生成多页应用的环境。

1. 多页应用的区别

单页应用这个概念,是随着前几年 AngularJS、React、Ember 等这些框架的出现而出现的。在前面的前言内容里,我们在页面渲染中讲了页面的局部刷新,而单页应用则是使用了页面的局部刷新的能力,在切换页面的时候刷新页面内容,从而获取更好的体验。

1.1 SPA 与 MPA

先从总体来看,单页应用(SinglePage Web Application,简称 SPA)和多页应用(MultiPage Application,简称 MPA)的区别如下:

表 14-1 单页应用与多页应用的区别

-SPAMPA
组成一个主页面 + 多个页面片段多个完整页面
资源共用(css,js)共用的资源只需要加载一次每个页面都需要加载公用的资源
url 模式xxx/#/page1
xxx/#/page2
xxx/page1.html
xxx/page2.html
刷新方式页面局部刷新或更改整页刷新
页面跳转外壳不变,更新局部页面内容,容易实现跳转动画从一个页面跳转到另一个页面,无法实现跳转动画
用户体验页面片段间切换快,用户体验好页面切换需要重新加载,比较慢且流畅度低,用户体验较差
数据传递同一个页面,全局变量等很容易实现依赖 url 传参、或者 cookie 、localStorage 等,实现麻烦
搜索引擎优化(SEO)实现较为困难,不利于 SEO 检索实现方法简单
适用场景对体验要求高的应用需要对搜索引擎友好的应用

可以看到的是,单页应用对 SEO 的支持依然会比多页应用要差,但是单页应用的页面体验会比多页应用好很多。

2. Webpack 实现多应用共享项目

一些 Webpack 的基本概念,我们已经在第2章中介绍过,主要包括入口(entry)、输出(output)、loader、插件(plugins)等等,这里就不再重复介绍了。Webpack 相关的能力其实远不止这些,感兴趣的小伙伴可以上官网或者查一些相关的文章进行查看和阅读。

这里我们要实现一个多应用共享项目,指的是多个单页应用共享一个项目(共享公共组件、公共库、构建工具和资源),而我们需要针对每个应用/页面进行单独构建打包。

2.1 项目配置

多应用项目具体是怎样的组织结构,首先我们得设计一下想要的目录组织,然后再根据这样的方式来看看具体的实现方式。

目录组织

我们想要的目录结构,应该是根据页面 Page 进行单独打包,但其他会有共享的一些资源(组件、工具库、静态资源等),同时每个应用自身也可以用于一些应用内的公共组件和公共库等,所以我们可以设计目录组织如下:

├── build/ # webpack配置参数文件 │ └── ... ├── src/ # 项目代码入口 │ │ │ ├── components # 多个项目共享的组件 │ ├── utils # 多个项目共享的工具库 │ ├── assets # 多个项目共享的静态资源 │ │ │ └── pages # 多个项目页面划分 │ ├── page1/ # 第一个页面或者应用 │ │ ├── main.js # 页面/应用入口文件 │ │ ├── components # 该页面/应用自身的组件 │ │ ├── utils # 该页面/应用自身的工具库 │ │ ├── main.js # 页面/应用入口文件 │ │ └── ... │ └── page2/ # 第二个页面或者应用 │ │ ├── main.js # 页面/应用入口文件 │ │ └── ... │ └── pageN/ # 第N个页面或者应用 │ ├── main.js # 页面/应用入口文件 │ └── ... ├── dist/ # 项目打包代码 │ ├── page1/ # 第一个页面或者应用 │ │ ├── [hash].js │ │ └── index.html # 页面/应用入口文件 │ ├── page2/ # 第二个页面或者应用 │ │ ├── [hash].js │ │ └── index.html # 页面/应用入口文件 │ └── pageN/ # 第N个页面或者应用 │ ├── [hash].js │ └── index.html # 页面/应用入口文件 ├── .babelrc # babel编译参数 ├── index.html # 主页模板,所有的页面共用该index.html入口 └── package.json # 项目文件,记载着一些命令和依赖还有简要的项目描述信息

这里我们可以看到我们项目代码的入口位于src文件夹,并且每个页面或者 app 都以目录名为页面的名字。而打包后的文件也一样,以目录为单位,支持单个打包或是全部打包。

基本配置文件

由于我们需要实现开发时多页面共同启动,打包时分块打包的功能,故在不同环境下我们的入口entryplugins等将会不一致,这里我们先省略:

// 这是一个常用的 Webpack 配置 var path = require("path"); // 配置包括入口(entry)、输出(output)、loader、插件(plugins)等 var webpackConfig = { entry: {}, output: { path: path.join(__dirname, "dist"), filename: "./[hash].js" }, resolve: { extensions: [".js", ".json"] // '.ts' and more }, module: { // 一些常用 loader rules: [ { test: /\.js$/, loader: "babel-loader", include: [path.resolve(__dirname, "./src")] } // more loaders... ] }, plugins: [] }; module.exports = webpackConfig;

2.2 获取目录名

既然目录名字会在我们的项目搭建中起这么重要的作用,这里我们就将它们获取存起来。

使用 glob 模块

这里我们将使用glob模块,它允许你使用*等符号,来写一个glob规则,像在 shell 脚本里一样,获取匹配对应规则的文件。

(1) 安装依赖。

npm i glob

(2) 使用方式。

var glob = require("glob"); // options可选 glob("**/*.js", options, function(er, files) { // files是匹配到文件的文件名数组 // 如果 `nonull` 选项被设置为true,而且没有找到任何文件 // 那么files就是glob规则本身,而不是空数组 // er是当寻找的过程中遇的错误 });

我们来看一下 glob 模块有哪些匹配规则(如果熟悉正则的你,相信也对这些规则了如指掌了):

表 14-2 glob 模块规则说明

规则说明
*匹配该路径段中 0 个或多个任意字符
?匹配该路径段中 1 个任意字符
[...]匹配该路径段中在指定范围内字符
`*(patternpattern
`!(patternpattern
`?(patternpattern
`+(patternpattern
`@(patternpattern
***一样,可以匹配任何内容,但**不仅匹配路径中的某一段,而且可以匹配'a/b/c'这样带有'/'的内容,所以它还可以匹配子文件夹下的文件

我们的期望是,解析目录结构,获取到目录名字,然后提供给其他模块使用。这是一个相对通用的能力,所以我们新建一个工具文件,然后将公共的方法管理起来。

utils

我们把这块获取目录名的功能作为工具单独管理起来,放在build/utils.js文件里,我们来看一下具体的实现:

// build/utils.js文件 var glob = require("glob"); function getEntries(globPath) { // 获取所有匹配文件的文件名数组 var files = glob.sync(globPath), entries = {}; files.forEach(function(filepath) { // 取倒数第二层(view下面的文件夹)做包名 var split = filepath.split("/"); var name = split[split.length - 2]; // 保存{'目录名': '目录路径'} entries[name] = "./" + filepath; }); return entries; } // 获取所有匹配src下目录的文件夹名字,其中文件夹里main.js为页面入口 var entries = getEntries("src/**/main.js"); module.exports = { entries: entries };

该方法最终返回获取到的目录名,我们就可以根据这些目录名来进行打包了。

2.3 相关 Npm 模块介绍

打包页面需要用到一些 npm 模块(需单独安装),这里我们简单介绍一下。

ora 模块

ora 模块主要用来实现 Node.js 命令行环境的 loading 效果,和显示各种状态的图标等。这里由于我们需要自行使用 Webpack 实现构建和打包能力,所以相应的一些终端输出和进度展示还是需要的,我们看一下简单的示例:

const ora = require("ora"); // 开始显示 const spinner = ora("Loading unicorns").start(); setTimeout(() => { // 一秒后设置颜色和内容 spinner.color = "yellow"; spinner.text = "Loading rainbows"; }, 1000);

rimraf 模块

rimraf 模块用于实现 Node.js 环境的 UNIX 命令rm -rf。一般来说,如果是简单的脚本我们也可以直接使用 shell 来实现,或者我们也可以使用一个 shelljs 的工具模块(一个 Node.js 环境的Unix shell命令),最终选择看开发者个人偏好。同样的,这里我们也看一下简单的使用方式:

rimraf(f, [opts], callback);

其中,一些参数可以参考以下表格:

表 14-3 rimraf 模块参数说明

参数名说明
f可为glob匹配规则的文件
[opts]一些选项,具体可参考官方说明
callback若执行过程中出错,则回调参数为error

chalk 模块

chalk 模块用于命令行输出各种样式的字符串。这里可以结合前面的 ora 模块,一起输出相应的进度状态,同时还可以设计输出的文字样式,用来做一些成功或是错误的提示。使用方式为chalk.<style>[.<style>...](string, [string...]),例如:

// 例如,红色带下划线的粗体字 chalk.red.bold.underline("Hello", "world");

2.4 Node.js 模块

前面介绍了一些需要单独安装和引入的模块库,这里我们介绍将使用到的 Node.js 自带 API 和内置模块(无需安装)。

path 模块

path 模块提供了一些工具函数,用于处理文件与目录的路径,这是 Node.js 一个自带的模块。path 模块的默认操作会根据 Node.js 应用程序运行的操作系统的不同而变化。比如,当运行在Windows操作系统上时,path 模块会认为使用的是Windows风格的路径。我们来看下常用的方法:

表 14-4 path 模块方法说明

方法介绍
path.join([...paths])使用平台特定的分隔符把全部给定的path片段连接到一起,并规范化生成的路径。例如path.join(__dirname, 'src')
path.parse(path)返回一个对象,对象的属性表示path的元素。返回属性包括:dir, root, base, name, ext
path.format(pathObject)会从一个对象返回一个路径字符串,与path.parse()相反
path.dirname(path)返回一个path的目录名,类似于Unix中的dirname命令

通过这个模块,我们就可以直接实现想要的文件处理和代码打包,不需要考虑兼容性了。

process 对象

process 对象是一个global(全局变量),提供有关信息,控制当前 Node.js 进程。作为一个对象,它对于 Node.js 应用程序始终是可用的,故无需使用require()。该模块的属性一般会包括:

表 14-5 process 对象属性介绍

属性介绍
process.execPath返回启动 Node.js 进程的可执行文件所在的绝对路径
process.argvprocess.argv属性返回一个数组,这个数组包含了启动 Node.js 进程时的命令行参数。第一个元素为process.execPath。如果需要获取argv[0]的值请参见process.argv0。第二个元素为当前执行的 JavaScript 文件路径,剩余的元素为其他命令行参数
process.envprocess.env属性返回一个包含用户环境信息的对象。
像我们经常看到生产环境process.env.NODE_ENV = 'production'和开发环境process.env.NODE_ENV = 'dev'
process.stdin输入流
process.stdout输出流
// 示例 // 运行以下命令,启动进程: $ node process-args.js one two=three four // process.argv 将输出: 0: /usr/local/bin/node 1: /Users/mjr/work/node/process-args.js 2: one 3: two=three 4: four

2.5 打包实现

好了,介绍了那么多的 Npm 模块和 Node.js 自带模块,我们来看一下这些模块功能要怎么配合实现想要的效果。其实在前端工程化的路上走,这些模块会成为常用的一些基本知识,不管是实现简单的打包构建,还是实现持续集成、自动化测试等能力,都是不可少的一些概念,在空闲的时候可以多自行学习和研究下,对个人的发展也是有不少好处的。

逻辑思路

不多说,我们来规划一下最终打包能实现的效果:
(1) 可输入目录名,来只打包对应的页面。
(2) 不输入目录名的时候,则将全部页面重新打包。

简单来说,就是:

  • 输入npm run build page1时,打包 page1 页面
  • 输入npm run build page1 page2时,打包 page1 和 page2 页面
  • 输入npm run build时,打包所有页面

前面也介绍了一些基本的 Npm 和 Node.js 模块能力,这里我们可以通过process.argv获取命令行参数。同时我们需要针对每个页面单独打包,这里我们将多个页面拆分成多个并行的任务,每个任务需要设置以下内容:

  • entry:设置单个页面入口
  • output.path:设置最终生成文件目录
  • plugins:设置打包后的 index.html,这里我们使用相同的 index.html 作为模板

代码实现

我们的页面打包代码放置在build文件夹下的build.js,则我们的package.json中的script

{ "scripts": { "build": "node build/build.js" } }

这样,我们的process.argv前两个参数分别是nodebuild/build.js,故我们需要先去掉前面两个参数,才能获取剩余页面参数:

var ora = require("ora"); var rm = require("rimraf"); var path = require("path"); var utils = require("./utils"); var chalk = require("chalk"); var webpack = require("webpack"); var webpackConfig = require("./webpack.config"); var HtmlWebpackPlugin = require("html-webpack-plugin"); var entries = utils.entries; var pageArray; // 取掉前两个参数,分别为node和build process.argv.splice(0, 2); if (process.argv.length) { // 若传入页面参数,则单页面打包 pageArray = process.argv; } else { // 若无传入页面参数,则全块打包 pageArray = Object.keys(entries); console.log(pageArray); } // 开始输出loading状态 var spinner = ora("building for production...\n"); spinner.start(); pageArray.forEach(function(val, index, array) { rm(path.join(__dirname, "..", "dist", val), err => { if (err) throw err; // print pageName[] console.log(index + ": " + val); // 输出目录dist/pageName webpackConfig.output.path = path.join(__dirname, "..", "dist", val); // 入口文件设定为指定页面的入口文件 // main.js这里为通用入口文件 webpackConfig.entry = {}; webpackConfig.entry[index] = path.join( __dirname, "..", "src", "pages", val, "main.js" ); // 添加index.html主文件 webpackConfig.plugins = [ new HtmlWebpackPlugin({ // 生成出来的html文件名 filename: "index.html", // 每个html的模版,这里多个页面使用同一个模版 template: "./index.html", // 或使用单独的模版 // template: './src/' + val + '/index.html', // 自动将引用插入html inject: true // 每个html引用的js模块,也可以在这里加上vendor等公用模块 // chunks: [name] }) ]; // 开启打包 webpack(webpackConfig, function(err, stats) { spinner.stop(); // 输出错误信息 if (err) throw err; // 输出打包完成信息 process.stdout.write( stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + "\n\n" ); console.log(chalk.cyan(" Build complete.\n")); console.log( chalk.yellow( " Tip: built files are meant to be served over an HTTP server.\n" + " Opening index.html over file:// won't work.\n" ) ); }); }); });

2.6 开发部署

我们先讲解打包的实现,原因是开发部署的功能会比打包的能力要复杂一些,例如需要实现 watch 能力,以及实时路由匹配的能力等。

逻辑思路

开发环境的部署和生产环境不一致,我们规划的本地环境实现的效果如下:
(1) 整个项目启动一次,多页面共享相同环境。
(2) 根据路由来匹配不同页面,路由与页面目录一致。

简单地说,可以理解为:

  • 路由为/page1时,打开 page1 页面
  • 路由为/page2时,打开 page2 页面
  • 路由匹配不到对应页面时,进行相关提示

接下来,我们会用到 Express 模块。

Express 模块与路由

Express 模块是一个基于 Node.js 平台的极简、灵活的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 应用。我们需要路由的匹配,这里我们使用 express 模块,首先我们需要了解下路由。

路由(Routing)是由一个 URI(或者叫路径)和一个特定的 HTTP 方法(GET、POST 等)组成的,涉及到应用如何响应客户端对某个网站节点的访问。每一个路由都可以有一个或者多个处理器函数,当匹配到路由时,这个函数将被执行。

路由的定义由如下结构组成:app.METHOD(PATH, HANDLER)。其中,app是一个 express 实例,METHOD是某个 HTTP 请求方式中的一个,PATH是服务器端的路径,HANDLER是当路由匹配到时需要执行的函数。

我们来看一个官方示例:

// 对网站首页的访问返回 "Hello World!" 字样 app.get("/", function(req, res) { res.send("Hello World!"); }); // 网站首页接受 POST 请求 app.post("/", function(req, res) { res.send("Got a POST request"); });

(1) 请求对象(req)部分属性。

表 14-6 req 属性介绍

属性介绍补充说明
req.params这是一个数组对象,命名过的参数会以键值对的形式存放比如有一个路由/user/:name"name"属性会存放在req.params.name, 这个对象默认为{}
req.query一个解析过的请求参数对象,默认为{}这个特性是bodyParser()中间件提供,其它的请求体解析中间件可以放在这个中间件之后。当bodyParser()中间件使用后,这个对象默认为{}
req.body这个对应的是解析过的请求体-
req.route这个对象里是当前匹配的 Route 里包含的属性比如原始路径字符串,产生的正则,等等
req.path返回请求的 URL 的路径名-
req.host返回从"Host"请求头里取的主机名,不包含端口号-

(2) 响应对象(res)部分属性。

表 14-7 res 属性介绍

属性介绍
res.end()终结响应处理流程
res.json()发送一个 JSON 格式的响应
res.jsonp()发送一个支持 JSONP 的 JSON 格式的响应
res.redirect()重定向请求
res.render()渲染视图模板
res.send()发送各种类型的响应
res.sendFile以八位字节流的形式发送文件
res.sendStatus()设置响应状态代码,并将其以字符串形式作为响应体的一部分发送

Express 的能力很强大,常常会用在服务端开发,包括常见的 HTTP 服务、Websocket 服务等,如果使用 Node.js 做类似聊天室等服务,Express 常常是主要选型方向之一,大家也可以多去了解一下。

代码实现

我们的开发部署代码放置在 build 文件夹下的 dev-server.js,则我们的 package.json 中的 script 设置如下:

{ "scripts": { "dev": "node build/dev-server.js", "build": "node build/build.js" } }

同时,我们将每个页面的主页面命名为[pageName].html,然后匹配路由之后就能获取相关页面:

// dev-server.js var path = require("path"); var express = require("express"); var utils = require("./utils"); var webpack = require("webpack"); var webpackConfig = require("./webpack.config"); var HtmlWebpackPlugin = require("html-webpack-plugin"); // Express实例 var app = express(); // 获取页面目录 var entries = utils.entries; // 重置入口entry webpackConfig.entry = {}; // 设置output为每个页面[name].js webpackConfig.output.filename = "[name].js"; webpackConfig.output.path = path.join(__dirname, "dist"); Object.keys(entries).forEach(function(name) { // 每个页面生成一个entry,如果需要HotUpdate,在这里修改entry webpackConfig.entry[name] = entries[name]; // 每个页面生成一个[name].html var plugin = new HtmlWebpackPlugin({ // 生成出来的html文件名 filename: name + ".html", // 每个html的模版,这里多个页面使用同一个模版 template: "./index.html", // 自动将引用插入html inject: true, // 每个html引用的js模块,也可以在这里加上vendor等公用模块 chunks: [name] }); webpackConfig.plugins.push(plugin); }); // webpack编译器 var compiler = webpack(webpackConfig); // webpack-dev-server中间件 var devMiddleware = require("webpack-dev-middleware")(compiler, { publicPath: "/", stats: { colors: true, chunks: false }, progress: true, inline: true, hot: true }); // 使用webpack中间件 app.use(devMiddleware); // 路由 app.get("/:pagename?", function(req, res, next) { var pagename = req.params.pagename ? req.params.pagename + ".html" : "index.html"; var filepath = path.join(compiler.outputPath, pagename); // 使用webpack提供的outputFileSystem compiler.outputFileSystem.readFile(filepath, function(err, result) { if (err) { // something error return next( "输入路径无效,请输入目录名作为路径,有效路径有:\n/" + Object.keys(entries).join("\n/") ); } // 发送获取到的页面 res.set("content-type", "text/html"); res.send(result); res.end(); }); }); module.exports = app.listen(8080, function(err) { if (err) { // do something return; } console.log("Listening at example.com\n"); });

这里面我们使用到 webpack-dev-middleware 模块,主要用于监视文件变化后重新编译,但这里并没有结合热加载来刷新页面,我们需要再添加一些 Webpack 插件来辅助实现。

2.7 Webpack 插件

Webpack 插件也在第2章进行过一些基本介绍,这里我们就直接介绍一些会用到的插件吧。

Express 与 Webpack

Express 本质是一系列 middleware 的集合,在这里比较适合 Express 的 Webpack 插件是 webpack-dev-middleware 和 webpack-hot-middleware,我们分别来介绍一下。

webpack-dev-middleware

webpack-dev-middleware 是一个处理静态资源的 middleware。有时候我们无需使用到 Express,我们常常使用 webpack-dev-server 开启动服务。 webpack-dev-server 实际上是一个小型 Express 服务器,它也是用 webpack-dev-middleware 来处理 Webpack 编译后的输出。

webpack-hot-middleware

webpack-hot-middleware 是一个结合 webpack-dev-middleware 使用的 middleware,它可以实现浏览器的无刷新更新(hot reload)。这也是 Webpack 文档里常说的 HMR(Hot Module Replacement)。

实现热加载和页面刷新

其实如果将热加载定义为文件变动时重新编译的话,其实我们前面已经完成了。但热加载的功能,完整的使用方法需要搭配页面自动刷新。因此,我们需要调整三个地方:
(1) 每个页面入口需要添加webpack-hot-middleware/client?reload=true
(2) 在 Webpack 配置中添加 plugin 插件new webpack.HotModuleReplacementPlugin()
(3) 在 Express 实例中添加中间件'webpack-hot-middleware'

我们的代码需要调整为:

// dev-server.js // 这里只注释介绍调整的部分,其他注释内容可参考前面的介绍 var path = require("path"); var express = require("express"); var utils = require("./utils"); var webpack = require("webpack"); var webpackConfig = require("./webpack.config"); var HtmlWebpackPlugin = require("html-webpack-plugin"); // 新增以下插件 var WebpackDevMiddleware = require("webpack-dev-middleware"); var WebpackHotMiddleware = require("webpack-hot-middleware"); var app = express(); var entries = utils.entries; // entry中添加HotUpdate地址 var hotMiddlewareScript = "webpack-hot-middleware/client?reload=true"; webpackConfig.entry = {}; webpackConfig.output.filename = "[name].js"; webpackConfig.output.path = path.join(__dirname, "dist"); Object.keys(entries).forEach(function(name) { // 每个页面生成一个entry // 这里修改entry实现HotUpdate webpackConfig.entry[name] = [entries[name], hotMiddlewareScript]; var plugin = new HtmlWebpackPlugin({ filename: name + ".html", template: "./index.html", inject: true, chunks: [name] }); webpackConfig.plugins.push(plugin); }); // 添加热加载插件 webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); var compiler = webpack(webpackConfig); // 添加两个插件中间件 app.use( WebpackDevMiddleware(compiler, { publicPath: "/", stats: { colors: true, chunks: false }, progress: true, inline: true, hot: true }) ); app.use(WebpackHotMiddleware(compiler)); app.get("/:pagename?", function(req, res, next) { // 这里没有调整,篇幅原因省略 }); module.exports = app.listen(8080, function(err) { // 这里同样的没有调整,篇幅原因省略 });

这样,我们就实现了代码的热加载以及页面自动刷新了。

除了基本的编译构建、打包部署等能力,我们还需要配备更多的选型能力,例如是否生成、如何生成 Source Map,同时还有代码压缩、hash 命名等能力的提供,其实大家也可以去 Webpack 官方看一下,官方也有推荐的配置、插件和相关的使用方式介绍,这里也不多讲啦,讲多了就从 Vue 的内容变成了 Webpack 的内容了。

说了那么多,我们来看一下最终的效果,首先是目录结构如图 14-8 :
image
图 14-1 目录结构

本地构建效果如图 14-9:
image
图 14-2 本地构建效果

本地构建页面效果如图 14-10:
image
图 14-3 本地构建页面效果

我们也可以指定打包某些页面:
image
图 14-4 指定打包某些页面效果

如果不带参数,则默认打包全部页面:
image
图 14-5 默认打包全部页面

3. Vue CLI 脚手架配置

上面讲的是使用 Webpack 自行实现多页应用的功能,而前面[《第2章 Vue 环境快速搭建》(./2.md)中我们介绍了其实官方的脚手架 Vue CLI 也是使用 Webpack,那么我们是否可以基于原有脚手架基础下进行调整,来实现同样的能力呢?

3.1 pages 选项

在 Vue CLI 的配置选项中,提供了 pages 选项,在多页模式下构建应用。同样的,每个“page”应该有一个对应的 JavaScript 入口文件。其值应该是一个对象,对象的key是入口的名字,value支持两种模式,分别是:
(1) 一个指定了entry, template, filename, titlechunks的对象 (除了entry之外都是可选的)。
(2) 一个指定其entry的字符串。

所以,其实前面写的一大堆 Webpack 功能,我们只需要这样配置就可以了:

// vue.config.js module.exports = { // 其他选项 pages: { page1: "src/pages/page1/main.js", page2: "src/pages/page2/main.js", page3: "src/pages/page3/main.js" } };

我们可以使用脚手架原配的命令来运行本地构建:
image
图 14-6 脚手架本地构建

同样地,也可以进行打包:
image
图 14-7 脚手架打包

3.2 自动生成 pages 路径

如果是使用上面的方式进行配置,每次我们变更页面名字、新增或是删除页面的时候,都需要手动进行更新,这我们可以同样地使用前面的路径获取来进行调整:

// vue.config.js var glob = require("glob"); function getEntries(globPath) { // 获取所有匹配文件的文件名数组 var files = glob.sync(globPath), entries = {}; files.forEach(function(filepath) { // 取倒数第二层(view下面的文件夹)做包名 var split = filepath.split("/"); var name = split[split.length - 2]; // 保存{'目录名': '目录路径'} entries[name] = filepath; }); return entries; } // 获取所有匹配src下目录的文件夹名字,其中文件夹里main.js为页面入口 var pages = getEntries("src/**/main.js"); console.log(pages); // 打印看看 module.exports = { // 其他选项 // pages 选项 pages: pages };

这样我们就可以自动查找对应的页面内容,然后生成对应的页面了:
image
图 14-8 自动生成对应的页面

页面效果,和前面 Webpack 实现的效果一致:
image
图 14-9 页面效果

显然,使用官方脚手架 Vue CLI 能节省不少的时间和力气,只需要短短的几行配置就能实现使用 Webpack 时费尽力气做出来的效果。但是这样是否意味着就不需要了解前面的内容呢?并不是这样的,其实我们也是在了解前面的方案之后,才能快速地进行自动生成 pages 路径这样的操作。而当我们遇到需要更加自定义实现的效果时,官方脚手架或许不再能简单进行支持,这时候掌握了 Webpack 方式的你就能快速地进行调整和实现。

工具使得做事情更快快速便捷,但并不意味着我们可以直接放弃更深层次的能力,只有在掌握工具的实现方式之后,才能更加自由地使用它。这里使用 Vue CLI 脚手架来进行多页开发,我们最终打包的页面结构是这样的:

image
图 14-10 打包后页面结构

如果现在你需要调整 Vue CLI 的配置,来实现上方 Webpack 所实现的效果(也就是每个页面作为单独的应用生成文件夹进行打包),你要怎么来做呢?