14. 搭建多页应用 3个月前

编程语言
375
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 单页应用与多页应用的区别

- 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 都以目录名为页面的名字。而打包后的文件也一样,以目录为单位,支持单个打包或是全部打包。

基本配置文件

由于我们需要实现开发时多页面共同启动,打包时分块打包的功能,故在不同环境下我们的入口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 个任意字符
[...] 匹配该路径段中在指定范围内字符
`*(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前两个参数分别是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 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 :
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 所实现的效果(也就是每个页面作为单独的应用生成文件夹进行打包),你要怎么来做呢?

image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2223510
累计阅读

热门教程文档

10.x
88小节
Spring Cloud
8小节
Linux
51小节
Kotlin
68小节
Maven
5小节