多页面应用(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
- 构建MPA时,我们约定 "entry" 值中的
key
的名称,作为页面入口目录,一个key
的名称,表示一个SPA
应用,所有与之相关的业务代码都放到这个目录里面。- 比如上面代码中, "entry" 值有两个
key
,分别是app
和appAdmin
。所以我们在工程中 "pages" 文件夹中新建两个文件夹app
和appAdmin
,与 "entry" 的key
相对应。
- 比如上面代码中, "entry" 值有两个
// 工程目录
|-- build/
|-- src/
|---- pages/
|------ app/index.js
|------ appAdmin/index.js
|-- babel.config.js
|-- package.json
|-- webpack.config.js
- 同时,html入口文件应该放在与 "entry"值的
key
相对应的文件夹中,配置 "html-webpack-plugin" 插件的template
和filename
。这样构建出来的才是多页面
// 工程目录
|-- 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/
- 按照上述操作完以后,打开
app/index.html
源码,你会发现里面引入了appAdmin
构建的内容,这样是不对的,应该是分开来引用的。app/index.html
只引用与app
相关资源,同样appAdmin
也是这样的。这里我们就需要设置 "html-webpack-plugin" 插件的chunks
属性来达到我们的目的。
chunks
属性作用指定页面插入哪些chunk
,默认值为all
,即会把entry
中所有构建好的chunk
插入到页面中,因此在MPA构建中,我们就需要为每个html文件指定chunks
属性了。注意,chunks
属性的值与entry
中key
的名称也是一一对应。
// 配置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
主要是根据我们约定好的多页面工程目录来动态的生成相应的 entry
和 html
,具体代码如下:
// 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继续更新 ღ( ´・ᴗ・` )比心