因为自己的项目是基于vue-cli3
进行开发,所以这里只讨论这种情况下的解决办法
在进行多页面开发的时候,项目刚开始阶段,页面较少,编译速度还能忍受,但是一旦页面增加,多次热更新就造成了内存溢出。
原因
这里需要借助一个插件来进行性能分析webpack-bundle-analyzer
,在vue.config.js
中添加以下代码
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
configureWebpack: {
plugins: [
new BundleAnalyzerPlugin(),
],
}
复制代码
下面是自己项目编译的截图
webpack
把所有的页面都进行了编译,总体积已经达到了18M
,耗时超过1分钟,在热更新的时候这个体积会变得更大,从而占据node
的运行内存,导致内存溢出。但是一般在开发的时候,我们一次更改的页面可能就只有几个,所以编译这些多余的页面是没有必要的。那下面就是多种解决方案
下面就是几种尝试的方法,加快编译的速度
增加Node
运行内存
在上面提到在热更新的时候,热更新的代码会大量占据node
分配的内存,导致内存溢出。那么第一种方式,尝试增加node
的运行内存。在Node
中通过JavaScript
使用内存时只能使用部分内存(64
位系统下约为1.4 GB
,32
位系统下约为0.7 GB
)。所以不管电脑实际的运行内存是多少,Node
在运行代码编译的时候,使用内存大小不会发生变化。这样就可能导致因为原有的内存不够,导致内存溢出。下面给出两种方案
更改cmd
在node_modules/.bin/vue-cli-server.cmd
把下面代码复制上去
@IF EXIST "%~dp0\node.exe" (
"%~dp0\node.exe" --max_old_space_size=4096 "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
) ELSE (
@SETLOCAL
@SET PATHEXT=%PATHEXT:;.JS;=;%
node --max_old_space_size=4096 "%~dp0\..\@vue\cli-service\bin\vue-cli-service.js" %*
)
复制代码
更改package.json
把启动Node
服务的更改下:
node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js serve
复制代码
本质上没什么区别,都是增加Node
分配的内存,在这里把node
的运行内存提高到4g
就能够让webpack
热编译的时候不会内存溢出。使用这种方法,确实是没有在出现内存溢出的情况。但是在首次启动和热编译的时候,速度并没有发生质的提升,首次编译还是达到了1分钟这种难以忍受的速度。如果项目进一步扩大,难道我们需要再次增加node
的运行内存?
过滤编译页面
在上面截图中可以看到,在编译的时候webpack
编译了一些不必要的页面,本来我们只需要调试A
页面,但是webpack
把所有的页面都进行了编译,某些页面我们可能并不需要,那这里就提出一种思路,对需要调试的页面进行过滤。
下面是多页面的配置:
// page.config.js
module.exports = {
index: {
entry: 'src/page/index/main.js', // 页面入口
template: 'public/index.html', // 页面模板路径
filename: 'index.html' // 输出文件名
title: '页面title',
}
}
// vue.config.js
const pages = require('./page.config.js')
module.expors = {
pages,
}
复制代码
可以看到的是传统的方案是把多页面的配置全部引入,进行编译,所以这里就提出一种解决方案,对多页面进行过滤,得到我们需要的编译页面,下面是过滤的脚本:
const path = require('path');
const fs = require('fs');
const pages = require('../pages.config');
const params = JSON.parse(process.env.npm_config_argv).original;
const buildPath = params[params.length - 1].match(/[a-zA-Z0-9]+/)[0] || '';
let buildConfig = {
pages: [],
};
if (!/(test|online|serve)/gi.test(buildPath)) {
const configJsPath = path.resolve(__dirname, `${buildPath}.js`);
// 如果该路径存在
if (fs.existsSync(configJsPath)) {
// eslint-disable-next-line import/no-dynamic-require
buildConfig = require(configJsPath);
} else if (pages[buildPath]) {
buildConfig.pages = buildPath.split(',');
} else {
throw new Error('该路径不存在');
}
} else {
buildConfig = require('./default');
}
const buildPages = {};
buildConfig.pages.forEach((name) => {
buildPages[name] = pages[name];
});
module.exports = buildPages;
复制代码
这样就可以单独单独编译我们所需要的页面下面是default.js
的内容:
module.exports = {
pages: ['ugcDetail']
}
复制代码
这个文件中pages
就是我们需要编译的文章,现在webpack
就过滤了不需要编译的页面,下面是页面编译速度的截图:
10
倍,因为需要编译的文件少了,所以运行速度也就提高了不少。一般情况下,一个人是负责单独的业务线,别人的代码我们也不需要干涉,所以也能实现一次配置,多次运行的效果。但是有的时候我们也需要更改别人的代码,不能说又加一个配置文件,下面就是借助webpack
自带的钩子实现编译指定文件。
使用webbpack-dev-serve
钩子进行单独编译
在上面page.config.js
中可以看到每个单页面都有一个入口文件,webpack
借助这些入口文件进行对每个页面进行单独编译,每个页面编译后的js
混合到一起也就非常大了,那我们能不能让这些入口文件暂时变成一个空文件,如果需要编译这个页面,在空文件中引入需要编译的入口文件。也就是所有的入口文件都变成了一个空的js
文件,如果需要编译这个页面,在通过
import 入口
复制代码
实现单独的页面文件编译。那webpack
是如何知道我们需要编译的页面呢,在webpack-dev-serve
中,有一个钩子before
,在访问页面的时候我们能够拿到页面信息的路径,下面是实现:
// vue.config.js
const compiledPages = [];
before(app) {
app.get('*.html', (req, res, next) => {
const result = req.url.match(/[^/]+?(?=\.)/);
const pageName = result && result[0];
const pagesName = Object.keys(multiPageConfig);
if (pageName) {
if (pagesName.includes(pageName)) {
if (!compiledPages.includes(pageName)) {
const page = multiPageConfig[pageName];
fs.writeFileSync(`dev-entries/${pageName}.js`, `import '../${page.tempEntry}'; // eslint-disable-line`);
compiledPages.push(pageName);
}
} else {
// 没这个入口
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end('<p style="font-size: 50px;">不存在的入口</p>');
}
}
next();
});
},
复制代码
多页面配置中以下配置,需要先把入口路径,先缓存起来,然后置空,下面是具体实现
{
pageName: {
entry: entryPath,
chunks: [array]
}
}
const fs = require('fs');
const util = require('util');
const outputFile = util.promisify(fs.writeFile);
async function main() {
const tasks = [];
if (!fs.existsSync('dev-entries')) {
fs.mkdirSync('dev-entries');
}
Object.keys(pages).forEach((key) => {
const entry = `dev-entries/${key}.js`;
pages[key].tempEntry = pages[key].entry; // 暂存真正的入口文件地址
pages[key].entry = entry;
tasks.push(outputFile(entry, ''));
});
await Promise.all(tasks);
}
if (process.env.NODE_ENV === 'development') {
main();
}
module.exports = pages;
复制代码
这种方法就是把所有页面入口文件置为空文件,虽然编译了所有的页面但是所有的文件都是空的,所以大大的减少了首次编译的文件大小。
80
多秒,降低到了8s
。然后当我们访问某个页面的页面,执行到before
钩子,进行单独编译,速度也是非常快的。
优化脚本
针对上面代码进行优化,上面实际上虽然把所以文件入口都制成了空,但是实际上还有这个空文件进行打包了,所以现在优化点在于只要这个页面没有调试,我们就不对他进行打包,这种方式还有个好处就是一般情况下都有测试环境,对于个人而言,很多时候需要把这个代码部署到测试机上。上面的方法解决了开发环境编译慢的情况,但是如果要把代码上传到测试机上,就一点帮助没有,需要上传所有的页面文件。所以给出下面优化的方式,这次我们不更改page.config.js
的代码。
在script
文件夹下面创建一个json
文件,里边存放一个最简单的页面入口,这个页面打包出来最好什么都没有。例如:
{
"miguRead": {
"entry": "src/pages/miguRead/main.js",
"template": "public/index.html",
"filename": "miguRead.html",
"title": "咪咕作业帮悦读会",
"business": "base"
}
}
复制代码
然后在修改vue.config.js
const developemntPages = require('./help')
let compiledPages;
if (process.env.NODE_ENV === 'development') {
compiledPages = developmentPages;
compiledPages.miguRead = multiPageConfig.miguRead;
} else {
compiledPages = multiPageConfig;
}
before: (app) {
//...
if (pageName) {
if (multiPageConfig[pageName]) {
if (!compiledPages[pageName]) {
compiledPages[pageName] = multiPageConfig[pageName];
fs.writeFileSync('help.json', `
${JSON.stringify(compiledPages)}
`);
}
} else {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end('<p style="font-size: 50px;">不存在的入口</p>');
}
}
}
复制代码
因为每次重启服务的时候,这些变量也会重置,所以需要借助一个不会重启而修改的文件,所以这里引入了一个help.json
。最后在package.json
中加入监听help.json
这个程序,当我们访问某个页面的时候,就会将该页面的信息写入help.json
。重启服务后,就能拿到该页面的信息。package.json
中更改的信息为
nodemon --watch vue.config.js --watch help.json --exec node --max_old_space_size=4096 node_modules/@vue/cli-service/bin/vue-cli-service.js serve
复制代码
升级html-webpack-plugin
版本
多页面出现内存溢出的问题是因为在编译的时候,实际是一次更改,编译了多个文件,这是html-webpack-plugin
的问题。因为没生成一个页面,就需要调用一下new htmlWebpackPlugin()
,多个页面的时候内存就不够用了。所以改一下这个这个webpack
插件的版本,升级到4.0.0-beta.8
这个版本。然后再vue.config.js
中添加下面的配置,这样也不会造成内存溢出。
const htmlPlugins = [];
Object.keys(multiPageConfig).forEach((key) => {
htmlPlugins.push(multiPageConfig[key])
})
configureWebpack: {
plugins: [
...htmlPlugins,
],
}
复制代码
单页面热编译过慢
后台技术栈使用vue-cli3
,由于后台项目汇聚了一个部门的后台服务,采用了单页面多路由的方案,所以导致路由特别多。刚开始路由较少的时候,编译起来速度还是挺快的,当页面增加到100
个的时候,那速度简直没法忍,比前台的多页面还慢,启动的时候需要等几分钟。所以在网上找了解决方法,需要在开发环境的时候将所有的异步组件都用同步的方式引入,具体为啥这么做,我也不知道,有兴趣的小伙伴可以研究一下。
解决方案
安装babel-plugin-dynamic-import-node
。它制作一件事就是将所有的import()
转换为require()
,这样就可以用这个插件将所有的异步组件都用同步的方式引入,并结合BABEL_ENV
这个babel
环境变量,让它只作用于开发环境下。具体做法如下,在babel.config.js
加入
env: {
development: {
plugins: ['dynamic-import-node'],
},
},
复制代码
vue-cli3
有对应的env
文件,例如我的就是.env.development
,然后添加环境变量
VUE_CLI_BABEL_TRANSPILE_MODULES=true
复制代码
OK,大功告成,编译速度快了15倍不止,赶快去试试吧。
这几种解决多页面内存溢出的方法各有优缺点,读者可根据自己的项目自行决定使用哪种方法,可能有时还需要多种方式组合使用,就看看那个好使好用了。
推销一波自己的github最近在抓紧学习,会持续更新文章,希望大家多多关注。
Log:
11-13
: 优化多页面打包方案,解决多路由编译慢