# 第14章 实战:使用 Webpack 或 Vue CLI 搭建多页应用

本章节相关代码存放在Github (opens new window)中。

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

# 14.1 多页应用的区别

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

# 14.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 的支持依然会比多页应用要差,但是单页应用的页面体验会比多页应用好很多。

# 14.2 Webpack 实现多应用共享项目

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

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

# 14.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;

# 14.2.2 获取目录名

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

# 使用 glob 模块

这里我们将使用glob模块 (opens new window),它允许你使用*等符号,来写一个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) 匹配括号中多个模型的 0 个或多个或任意个的组合
!(pattern|pattern|pattern) 匹配不包含任何模型
?(pattern|pattern|pattern) 匹配多个模型中的 0 个或任意 1 个
+(pattern|pattern|pattern) 匹配多个模型中的 1 个或多个
@(pattern|pattern|pattern) 匹配多个模型中的任意 1 个
** *一样,可以匹配任何内容,但**不仅匹配路径中的某一段,而且可以匹配'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
};

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

# 14.2.3 相关 Npm 模块介绍

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

# ora 模块 (opens new window)

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 模块 (opens new window)

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

rimraf(f, [opts], callback);

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

表 14-3 rimraf 模块参数说明

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

# chalk 模块 (opens new window)

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

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

# 14.2.4 Node.js 模块

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

# path 模块 (opens new window)

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 对象 (opens new window)

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

# 14.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"
        )
      );
    });
  });
});

# 14.2.6 开发部署

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

# 逻辑思路

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

简单地说,可以理解为:

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

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

# Express 模块 (opens new window)与路由

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 插件来辅助实现。

# 14.2.7 Webpack 插件

Webpack 插件也在《第2章 Vue 环境快速搭建》进行过一些基本介绍,这里我们就直接介绍一些会用到的插件吧。

# Express 与 Webpack

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

# webpack-dev-middleware (opens new window)

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

# webpack-hot-middleware (opens new window)

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 默认打包全部页面

点击此处查看源码 (opens new window)

# 14.3 Vue CLI 脚手架配置

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

# 14.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 脚手架打包

# 14.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 打包后页面结构

点击此处查看源码 (opens new window)

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