我们在第2章中简单介绍过 Webpack 的基本概念,以及 Vue CLI 的基本使用方式。本章会介绍如何使用 Webpack 或者 Vue CLI 搭建生成多页应用的环境。
1. 多页应用的区别
单页应用这个概念,是随着前几年 AngularJS、React、Ember 等这些框架的出现而出现的。在前面的前言内容里,我们在页面渲染中讲了页面的局部刷新,而单页应用则是使用了页面的局部刷新的能力,在切换页面的时候刷新页面内容,从而获取更好的体验。
1.1 SPA 与 MPA
先从总体来看,单页应用(SinglePage Web Application,简称 SPA)和多页应用(MultiPage Application,简称 MPA)的区别如下:
表 14-1 单页应用与多页应用的区别
- | SPA | MPA |
---|---|---|
组成 | 一个主页面 + 多个页面片段 | 多个完整页面 |
资源共用(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 都以目录名为页面的名字。而打包后的文件也一样,以目录为单位,支持单个打包或是全部打包。
基本配置文件
由于我们需要实现开发时多页面共同启动,打包时分块打包的功能,故在不同环境下我们的入口entry
和plugins
等将会不一致,这里我们先省略:
// 这是一个常用的 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 个任意字符 |
[...] |
匹配该路径段中在指定范围内字符 |
`*(pattern | pattern |
`!(pattern | pattern |
`?(pattern | pattern |
`+(pattern | pattern |
`@(pattern | pattern |
** |
和* 一样,可以匹配任何内容,但** 不仅匹配路径中的某一段,而且可以匹配'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.argv |
process.argv 属性返回一个数组,这个数组包含了启动 Node.js 进程时的命令行参数。第一个元素为process.execPath 。如果需要获取argv[0] 的值请参见process.argv0 。第二个元素为当前执行的 JavaScript 文件路径,剩余的元素为其他命令行参数 |
process.env |
process.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
前两个参数分别是node
和build/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 http://localhost:8080\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 :
图 14-1 目录结构
本地构建效果如图 14-9:
图 14-2 本地构建效果
本地构建页面效果如图 14-10:
图 14-3 本地构建页面效果
我们也可以指定打包某些页面:
图 14-4 指定打包某些页面效果
如果不带参数,则默认打包全部页面:
图 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
, title
和chunks
的对象 (除了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" } };
我们可以使用脚手架原配的命令来运行本地构建:
图 14-6 脚手架本地构建
同样地,也可以进行打包:
图 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 };
这样我们就可以自动查找对应的页面内容,然后生成对应的页面了:
图 14-8 自动生成对应的页面
页面效果,和前面 Webpack 实现的效果一致:
图 14-9 页面效果
显然,使用官方脚手架 Vue CLI 能节省不少的时间和力气,只需要短短的几行配置就能实现使用 Webpack 时费尽力气做出来的效果。但是这样是否意味着就不需要了解前面的内容呢?并不是这样的,其实我们也是在了解前面的方案之后,才能快速地进行自动生成 pages 路径这样的操作。而当我们遇到需要更加自定义实现的效果时,官方脚手架或许不再能简单进行支持,这时候掌握了 Webpack 方式的你就能快速地进行调整和实现。
工具使得做事情更快快速便捷,但并不意味着我们可以直接放弃更深层次的能力,只有在掌握工具的实现方式之后,才能更加自由地使用它。这里使用 Vue CLI 脚手架来进行多页开发,我们最终打包的页面结构是这样的:
图 14-10 打包后页面结构
如果现在你需要调整 Vue CLI 的配置,来实现上方 Webpack 所实现的效果(也就是每个页面作为单独的应用生成文件夹进行打包),你要怎么来做呢?