前端多页面打包通用方案
一、多页面应用(MPA)概念
每⼀次⻚⾯跳转的时候,后台服务器都会给返回⼀个新的
html
⽂档,
这种类型的⽹站也就是多⻚⽹站,也叫做多⻚应⽤。
二、多⻚⾯打包基本思路
每个⻚⾯对应⼀个
entry
,⼀个html-webpack-plugin
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js ‘ }
};
缺点:每次新增或删除⻚⾯需要改
webpack
配置
三、多⻚⾯打包通⽤⽅案
动态获取
entry
和设置html-webpack-plugin
数量
利⽤glob.sync
// 安装
npm i glob -D
// 使用
entry: glob.sync(path.join(__dirname, './src/*/index.js'))
四、多⻚⾯打包实操
1.划分好目录
src
目录下建立页面模块名称,页面目录下建立同名称子文件index.html
和index.js
(方便查找)
2.安装并引入glob
和HtmlWebpackPlugin
等
// 安装
npm i glob -D
npm i html-webpack-plugin -D
// 引入
const glob = require('glob');
const HtmlWebpackPlugin = require('html-webpack-plugin');
3.编写设置多页面函数(在webpack.js
文件中)
const setMPA = () => {
const entry = {};
const htmlWebpackPlugins = [];
const entryFiles = glob.sync(path.join(__dirname, './src/*/index.js'));
Object.keys(entryFiles)
.map((index) => {
const entryFile = entryFiles[index];
const match = entryFile.match(/src\/(.*)\/index\.js/);
const pageName = match && match[1];
entry[pageName] = entryFiles;
htmlWebpackPlugins.push(
new HtmlWebpackPlugin({
template: path.join(__dirname, `src/${pageName}/index.html`),
filename: `${pageName}.html`,
chunks: [pageName],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
)
})
return {
entry,
htmlWebpackPlugins
}
}
4.使用多页面参数及配置入口参数
// 获取参数
const { entry, htmlWebpackPlugins } = setMPA();
// 配置入口参数
module.exports = {
// 多页配置
entry: entry,
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
}
// 使用htmlWebpackPlugins插件
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new CleanWebpackPlugin(),
new HTMLInlineCSSWebpackPlugin(),
].concat(htmlWebpackPlugins) // 把获取到的插件加进来
五、项目中的多页面方案
const glob = require('glob')
const HtmlWebpackInlinePlugin = require('html-webpack-inline-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const titles = require('./titles.js')
const OfflinePackagePlugin = require('@vivo/offline-pkg')
// 取得相应的页面路径,因为之前的配置,所以是 src 文件夹下的 pages 文件夹
const isProd = process.env.NODE_ENV === 'production'
const isTest = process.env.NODE_ENV === 'test'
const isAnalyz = process.env.analyz === 'true'
// 取所有入口文件
let entryFiles = glob.sync('./src/views/**/main.js')
let pages = {}
entryFiles.forEach(filePath => {
let template = filePath.substring(0, filePath.lastIndexOf('/'))
let filename = template.replace('./src/views/', '')
let conf = {
// page 的入口
entry: filePath,
// 模板来源
template: template + '/index.html',
// 在 dist的输出
filename: filename + '.html',
title: titles[filename],
// 页面模板需要加对应的js脚本,如果不加这行则每个页面都会引入所有的js脚本
chunks: ['vendors', 'common', filename],
inject: true,
// 自定义版本
version: '0.0.2',
env: isProd, // 环境
dnsPrefetch: ['//topicstatic.vivo.com.cn/', '//mstatic.vivojrkj.com/'],
cdn: {
js: [
'//wwwstatic.vivojrkj.com/assets/__cdn/js/vue/v2.6.10/vue.runtime.min.js',
'//wwwstatic.vivojrkj.com/assets/__cdn/js/axios/v0.18.0/axios.min.js',
'//wwwstatic.vivojrkj.com/assets/__cdn/js/jsencrypt/v3.0.0/jsencrypt.min.js',
],
},
}
pages[filename] = conf
})
module.exports = {
pages: pages,
// 是否生成sourcemap文件,生成环境不生成以加速生产环境构建
productionSourceMap: !isProd,
// 静态资源文件的目录
assetsDir: 'static',
publicPath: process.env.VUE_APP_PUBLIC_PATH,
// css是否开启sourcemap,生成环境不生成
css: {
sourceMap: !isProd,
},
devServer: {
open: true,
index: '/',
before: app => {
app.get('/', (req, res, next) => {
for (let i in pages) {
res.write(`<a target="_self" href='${i}.html'>/${i}</a></br>`)
}
res.end()
})
},
proxy: {
'/': {
target: 'http://vivopay.vivo.com.cn/',
changeOrigin: true,
pathRewrite: {
'^': '',
}
},
'/api/subscription/': {
target: 'http://vivopay.vivo.com.cn/',
changeOrigin: true,
pathRewrite: {
'^': '',
}
},
}
},
configureWebpack: config => {
config.resolve.extensions = ['.js', '.vue', '.json']
config.plugins.push(new HtmlWebpackInlinePlugin())
config.plugins.push(
new HtmlWebpackPlugin({
minify: {
removeComments: true,
},
})
)
if (isProd) {
config.plugins.push(
new OfflinePackagePlugin({
htmlUrl: 'https://h5.vivo.com.cn/wallet/member/',
baseUrl: 'https://h5.vivo.com.cn/wallet/member/',
fileTypes: ['html', 'js', 'css', 'png'],
})
)
}
if (isTest) {
config.plugins.push(
new OfflinePackagePlugin({
htmlUrl: 'https://h5-inside.vivo.com.cn/wallet/member/',
baseUrl: 'https://h5-inside.vivo.com.cn/wallet/member/',
fileTypes: ['html', 'js', 'css', 'png'],
})
)
}
if (isAnalyz) {
config.plugins.push(
new BundleAnalyzerPlugin({
analyzerMode: 'server',
analyzerHost: '127.0.0.1',
analyzerPort: 8888,
reportFilename: 'report.html',
defaultSizes: 'parsed',
openAnalyzer: true,
generateStatsFile: false,
statsFilename: 'stats.json',
statsOptions: null,
logLevel: 'info',
})
)
}
},
chainWebpack: config => {
// 压缩代码
config.optimization.minimize(true)
// 分割代码
config.optimization.splitChunks({
cacheGroups: {
vendors: {
chunks: 'all',
test: /(core-js)/,
priority: 100,
name: 'vendors',
},
'async-commons': {
chunks: 'async',
minChunks: 2,
name: 'async-commons',
priority: 90,
},
common: {
chunks: 'all',
minChunks: 2,
name: 'common',
priority: 80,
},
},
})
if (process.env.NODE_ENV === 'development') {
config.output.filename('[name].[hash].js').end();
}
// 删除preload
Object.keys(pages).forEach(key => {
config.plugins.delete(`preload-${key}`)
})
// 用cdn方式引入
config.externals({
vue: 'Vue',
axios: 'axios',
jsencrypt: 'JSEncrypt',
})
},
}
// 引用 npm html-webpack-inline-plugin包,用来将html中inline标识的<script>,<link>,<img>标签的元素内容压缩进html中
const path = require('path');
const HtmlWebpackInlinePlugin = require('html-webpack-inline-plugin');
const pluginOptions = {
// 项目名,定义成我们在云平台申请的应用名,类似 ***.vivo.com.cn
projectName: 'vivopay.vivo.com.cn'
};
const isProd = process.env.NODE_ENV === 'production';
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
pages: {
index: {
entry: 'src/pages/index/main.js',
template: 'public/index.html',
filename: 'index.html',
chunks: ['libs', 'index']
},
bankCard: {
entry: 'src/pages/bankCard/main.js',
template: 'public/bankCard.html',
filename: 'bankCard.html',
chunks: ['libs', 'bankCard']
}
},
pluginOptions,
lintOnSave: false,
// 是否生成sourcemap文件,生成环境不生成以加速生产环境构建
productionSourceMap: !isProd,
// 静态资源文件的目录
assetsDir: 'static',
publicPath: process.env.VUE_APP_PUBLIC_PATH,
// css是否开启sourcemap,生成环境不生成
css: {
sourceMap: !isProd
},
devServer: {
open: true,
port: 8200,
disableHostCheck: true,
proxy: {
'/h5': {
target: process.env.VUE_APP_API_URL,
pathRewrite: { '^/proxy': '' }
},
'/user_center': {
target: process.env.VUE_APP_API_URL2,
pathRewrite: { '^/proxy': '' }
},
'/security_center': {
target: process.env.VUE_APP_API_URL2,
pathRewrite: { '^/proxy': '' }
},
'/api': {
target: process.env.VUE_APP_API_URL2,
pathRewrite: { '^/proxy': '' }
}
},
historyApiFallback: {
rewrites: [
{ from: /^\/bankCard/, to: '/bankCard.html' }
]
}
},
configureWebpack: config => {
config.plugins.push(
new HtmlWebpackInlinePlugin()
);
if (process.env.VUE_APP_ENV === 'prod') {
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
}
},
chainWebpack: config => {
// 压缩代码
config.optimization.minimize(true);
// 分割代码
config.optimization.splitChunks({
cacheGroups: {
libs: {
name: 'libs',
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
priority: 10
}
}
});
// 用cdn方式引入
config.externals({
vue: 'Vue',
'vue-router': 'VueRouter',
axios: 'axios'
});
// alias
config.resolve.alias
.set('@', resolve('src'));
// 设置全局 less 变量
const oneOfsMap = config.module.rule('less').oneOfs.store;
oneOfsMap.forEach(item => {
item
.use('sass-resources-loader')
.loader('sass-resources-loader')
.options({
resources: ['./src/style/vars.less']
})
.end();
});
},
// 很重要,默认不会babel nodemodules
transpileDependencies: ['@vivo/buffett', 'asn1.js']
};