多页面应用(MPA)开发最佳实践

缘由

平常开发当中,一般使用vue-cli2或vue-cli3脚手架来进行开发,默认构建出来的应用是单页面应用程序(SPA)。 面对一个工程下面只有一个应用的项目,这样做是没有问题的,而面对实际开发中多个页面的需求时,就会有它局限性。
比如一个项目中分为Mobile端和PC端,如果采用单页面模式构建的话,就会有两种开发方案:

  • 单独分开,新建两个工程
    新建两个工程的话,好处显而易见的,因为Mobile端和PC端从设计到交互到开发实现都是有差别的,分开的话,代码职责更明确一些,不会出现PC端中有一些Mobile端才需要的库,工程目录简洁明了,但无形中增加了,工程维护、联合调试和部署方面的工作

  • 融合在一起,共用一个工程 相对于"新建两个工程",节省了运维方面工作,像vue、vue-router、vuex等核心库也能共用。但缺点也很明显,代码职责不够明确,PC端代码和Mobile端代码容易冗在一起

那有没有结合上面两个方案的优点,平衡下缺点的解决方案呢?答案是有的,那就是多页面应用(MPA)。
MPA 首先它是一种应用的构建模式,是单页面应用(SPA)的扩展,弥补了单页面应用构建的不足。下面就结合在SBPO Webchat项目的真实实践,分享下Vue多页面应用(MPA)开发的最佳实践。

MPA核心 - entry

"entry" 是Webpack的一个配置属性,即webpack构建的入口起点,指示webpack应该使用哪个模块来作为构建的开始。
"entry" 属性值可以为一个String类型,也可以是Array类型,还可以是Object类型。

  • 当值为String类型时,表示当前构建只有一个入口
  • 当值为Array类型时,表示当前构建有只有一个入口,会把把数组中的元素打包成一个 "bundle"
  • 当值为Object类型时,表示当前构建有多个入口,这就是MPA构建关键
// webpack.config.js

// String
module.exports = {
    entry: './path/to/my/entry/file.js',
};

// Array
module.exports = {
    entry: ['./src/file_1.js', './src/file_2.js'],
};

// Object
module.exports = {
    entry: {
        app: './src/app.js',
        adminApp: './src/adminApp.js',
    },
};

MPA核心 - html-webpack-plugin插件

配置好 "entry" 后,后面一个核心就是 "html-webpack-plugin" 插件了。"html-webpack-plugin" 插件的作用是生成html入口文件,动态引入构建好的 "bundle"。所谓多页面也就是多个html入口文件,因此需要调用多次 "html-webpack-plugin" 插件。

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 访问内置的插件
const path = require('path');

module.exports = {
    entry: {
        app: './src/app.js',
        adminApp: './src/adminApp.js',
    },
    output: {
        filename: 'my-first-webpack.bundle.js',
        path: path.resolve(__dirname, 'dist'),
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                use: 'babel-loader',
            },
        ],
    },
    plugins: [
        new webpack.ProgressPlugin(),
        new HtmlWebpackPlugin({template: './src/app/index.html'}),
        new HtmlWebpackPlugin({template: './src/adminApp/index.html'}),
    ],
};

Tips

  1. 构建MPA时,我们约定 "entry" 值中的 key 的名称,作为页面入口目录,一个 key 的名称,表示一个 SPA 应用,所有与之相关的业务代码都放到这个目录里面。
    • 比如上面代码中, "entry" 值有两个 key,分别是 appappAdmin。所以我们在工程中 "pages" 文件夹中新建两个文件夹 appappAdmin,与 "entry" 的 key 相对应。
// 工程目录
|-- build/
|-- src/
|---- pages/
|------ app/index.js
|------ appAdmin/index.js
|-- babel.config.js
|-- package.json
|-- webpack.config.js
  1. 同时,html入口文件应该放在与 "entry"值的 key 相对应的文件夹中,配置 "html-webpack-plugin" 插件的 templatefilename。这样构建出来的才是多页面
// 工程目录
|-- build/
|-- src/
|---- pages/
|------ app/
|-------- index.html
|-------- index.js
|------ appAdmin/
|-------- index.html
|-------- index.js
|-- babel.config.js
|-- package.json
|-- webpack.config.js
// 配置html-webpack-plugin插件
module.exports = {
    //...
    plugins: [
        new webpack.ProgressPlugin(),
        new HtmlWebpackPlugin({
            template: './src/app/index.html',
            filename: './src/app/index.html'
        }),
        new HtmlWebpackPlugin({
            template: './src/adminApp/index.html',
            filename: './src/adminApp/index.html'
        }),
    ],
};
// 构建后生成的文件
|-- dist/
|---- app/index.html
|---- appAdmin/index.html
|---- statics/
  1. 按照上述操作完以后,打开 app/index.html 源码,你会发现里面引入了 appAdmin 构建的内容,这样是不对的,应该是分开来引用的。app/index.html 只引用与 app 相关资源,同样 appAdmin 也是这样的。这里我们就需要设置 "html-webpack-plugin" 插件的 chunks 属性来达到我们的目的。
  • chunks 属性作用指定页面插入哪些 chunk,默认值为 all,即会把 entry 中所有构建好的 chunk 插入到页面中,因此在MPA构建中,我们就需要为每个html文件指定 chunks 属性了。注意,chunks 属性的值与 entrykey的名称也是一一对应。
// 配置html-webpack-plugin插件 chunks属性
module.exports = {
    //...
    plugins: [
        new webpack.ProgressPlugin(),
        new HtmlWebpackPlugin({
            template: './src/app/index.html',
            filename: './src/app/index.html',
            chunks: ['app']
        }),
        new HtmlWebpackPlugin({
            template: './src/adminApp/index.html',
            filename: './src/adminApp/index.html',
            chunks: ['adminApp']
        }),
    ],
};

multientry.js

如果项目中页面少的话,可以按照上面步骤手动配置。但是页面多的话,还是手动的话就不容易维护和修改了。因此我们需要创建一个 multientry.js,来帮我们做这些事情。
multientry.js 主要是根据我们约定好的多页面工程目录来动态的生成相应的 entryhtml,具体代码如下:

// multientry.js
const fs = require('fs')
const path = require('path');
const glob = require("glob");
const HtmlWebpackPlugin = require('html-webpack-plugin');

const defaultEntrys = ['index', 'main', 'app'] // 默认entry名称

module.exports = (dir) => {
    const files = glob.sync(`${dir}/*/*.js`);
    const entryInfos = files.map(f => {
        let name = path.basename(f, '.js')
        let defaultEntry = false
        if (defaultEntrys.indexOf(name) > -1) {
            const pathSnippets = path.dirname(f).split('/')
            name = pathSnippets[pathSnippets.length - 1]
            defaultEntry = true
        }
        return {
            filename: f,
            name,
            defaultEntry
        }
    })

    const entrys = {}, pages = [], contextPath = path.dirname(dir)
    entryInfos.forEach((en) => {
        entrys[en.name] = [en.filename];
        let htmlPath = `${path.dirname(en.filename)}/index.html`
        if (!fs.statSync(htmlPath)) {
            htmlPath = `${path.dirname(en.filename)}/${en.name}.html`
        }

        pages.push(new HtmlWebpackPlugin({
            template: htmlPath,
            filename: `./${en.name}/index.html`,
            inject: true,
            // favicon: `${contextPath}/favicon.ico`,
            //压缩配置
            minify: {
                removeComments: true,//删除Html注释
                collapseWhitespace: true, //去除空格
                removeAttributeQuotes: true //去除属性引号
            },
            chunksSortMode: 'auto',
            chunks: [en.name],
            multihtmlCache: true
        }));
    });

    return {
        entrys,
        pages
    }
}

然后在 "webpack.config.js" 中调用

const webpack = require('webpack'); // 访问内置的插件
const path = require('path');
const multientry = require('./multientry'); // multientry.js

module.exports = {
    entry: multientry.entrys,
    // ...
    plugins: [new webpack.ProgressPlugin()].concat(multientry.pages),
};

结束

至此结束,后面有新的Tips继续更新 ღ( ´・ᴗ・` )比心

posted @ 2023-08-23 11:17  Khadron  阅读(170)  评论(0编辑  收藏  举报