Skip to content

Vue Cli 构建多页应用

背景

大多数的应用场景为单页面,但是到目前我遇到两种需要构建多页面应用的场景:

  1. 业务需要打包出不同模块,模块之间各自独立,支持独立部署;
  2. 项目过大,为提升性能分模块构建

两种场景的过程基本一致,下面以一个demo进行演示,其中会涉及到一些webpack配置相关和脚手架知识。

构建目标

确定大方向后需要细致化目标,拆解需求:

  • [ ] 区分模块,配置多入口打包
  • [ ] 可定制化哪些模块需要被打包
  • [ ] 抽离公共资源
  • [ ] 排查性能问题

构建过程

创建项目

创建vue-mpa项目并创建模块目录:

vue create vue-mpa
* vue-mpa/
  * src/
    * modules/
      * module1
        * app.js
        * app.vue
      * module2
        * app.js
        * app.vue
    * app.vue
    * main.js
  * README.md
  * package.json

创建了两个子模块module1module2,模块中的app.js挂载同目录下的app.vue文件。

js
import Vue from 'vue'
import App from './app.vue'

new Vue({
  el: '#app',
  render: h => h(App)
})

app.vue的内容可以自定义。

确定打包结构

  • dist
    • js 公共 公共内容,公共组件,公共方法(包括公共指令等)
    • css 公共 公共css
    • index.html 作为外层框架入口,主入口
    • moduleA
      • index.html
    • moduleB
      • index.html

修改打包

  1. 获取多模块的入口 首先vue-cli官方当已经提供了在 multi-page 模式下构建应用的配置选项pages: 给出的示例如下:
js
module.exports = {
  pages: {
    index: {
      // page 的入口
      entry: 'src/index/main.js',
      // 模板来源
      template: 'public/index.html',
      // 在 dist/index.html 的输出
      filename: 'index.html',
      // 当使用 title 选项时,
      // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
      title: 'Index Page',
      // 在这个页面中包含的块,默认情况下会包含
      // 提取出来的通用 chunk 和 vendor chunk。
      chunks: ['chunk-vendors', 'chunk-common', 'index']
    },
    // 当使用只有入口的字符串格式时,
    // 模板会被推导为 `public/subpage.html`
    // 并且如果找不到的话,就回退到 `public/index.html`。
    // 输出文件名会被推导为 `subpage.html`。
    subpage: 'src/subpage/main.js'
  }
}

结合示例和我们自定义的模块目录,抽取一下获取多模块入口的逻辑到/src/build/pageFiles中:

js
const path = require('path');

// glob 是 webpack 安装时依赖的一个第三方模块,该模块允许你使用 * 等符号,
// 例如 lib/*.js 就是获取 lib 文件夹下的所有 js 后缀名的文件
const glob = require('glob');

// 取得相应的页面路径,因为之前的配置,所以是 src 文件夹下的 modules 文件夹
const PAGE_PATH = path.resolve(__dirname, '../src/modules');

/*
* 多入口配置
* 通过 glob 模块读取 pages 文件夹下的所有对应文件夹下的 js * 后缀文件,如果该文件存在
* 那么就作为入口处理
*/
exports.getPages = () => {
  const pagesConfig = {}
  glob.sync(PAGE_PATH + '/**/app.js').forEach((filePath) => {
    let chunk = filePath.split('/src/modules/')[1].split('/app.js')[0]
    pagesConfig[chunk] = {
      entry: filePath,
      template: 'public/index.html',
      filename: `${chunk}/index.html`,
      title: 'module',
      chunks: ['chunk-vendors', 'chunk-common', chunk]
    }
  })

  return pagesConfig
}

vue.config.js文件中执行getPages获取入口配置:

js
const pageFiles = require('./build/pageFiles')

module.exports = defineConfig({
  publicPath: process.env.NODE_ENV === 'production' ? '../' : '/',
  pages: pageFiles.getPages(),
  // ...
})

执行npm run serve本地启动后,生成的多入口配置为: 本地访问主入口页面: 访问子模块页面: 执行npm run build最后的打包文件目录为:

扩展

上面的示例根据给定的目录直接获取了所有的子模块入口,但一般需求是根据权限列表去生成对应的模块资源,即可构建需要的模块,最简单的方式是命令行直接获取参数,比如我们可以在npm run build 后追加模块参数,但这种方式不够直观,因此使用inquirer实现一个交互式命令,供用户选择本次需要启动的模块列表。

交互式命令选择模块

npm i inquirer@8.2.6 --save-dev

这里下载的是v8版本,兼容cjs,v9不兼容。

新建generate-pages文件,用来生成用户手动选择的模块:

js
const inquirer = require('inquirer')
const pageFiles = require('./pageFiles')

exports.promptModule = async () => {
  const allModules = pageFiles.getPages()
  const answers = await inquirer.prompt([{
    type: 'checkbox',
    name: 'modules',
    message: '请选择启动的模块, 点击上下键选择, 按空格键确认(可以多选), 回车运行。',
    pageSize: 15,
    choices: Object.keys(allModules).map((item) => {
      return {
        name: item,
        value: item,
      }
    })
  }])

  let selectedPages = {}
  answers.modules.forEach(item => {
    selectedPages[item] = allModules[item]
  });

  return selectedPages
}

修改vue.config.js文件,异步生成页面配置:

js
const generatePages = require('./build/generate-pages')
module.exports = async () => {
  const dynamicPages = await generatePages.promptModule();
  return {
    // ...
    pages: dynamicPages,
     // ...
  }
}

执行打包: 只选择module2后,dist只包含module2资源。