问题背景
我们的客户开发的系统会销售给多个不同的单位使用,并且是需要私有化部署的。在有的客户那里,直接部署完就结束了。但是另外一些客户,提出了一些特别的要求。他们要求我们的系统只需要提供一个个功能页面,无需提供菜单管理等功能。功能页面的调度、管理、权限等工作,则是由他们内部的大平台来统一来完成。客户的这个需求比较特别,他们的这种需求,意味着我们必须要把后台做成一个MPA(Multiple Page Application,多页面应用)应用。但是我们实际上开发的已经是一个SPA(Single Page Application)应用了,且是无法直接满足客户需求的。同时,我们也不可能的大面积的去改动代码或者框架去适应客户的需求,这样会导致后续的开发越来越慢,越来越艰难。因此我们决定从打包入手,来解决这个问题。
解决问题的思路
通过对打包过程的深入介入,实现同样的一份代码,能够根据不同的打包方法,分别打包出SPA和MPA应用。
涉及到的主要技术
- Vue3
- Vite
- rollup
注:Vite是基于rollup的。
源代码结构(不包含打包以及工程等文件)
│ index.html
└─src
│ App.vue
│ main.js
│ style.css
├─assets
│ vue.svg
├─components
│ HelloWorld.vue
├─pages
│ ├─report
│ │ index.vue
│ ├─tdm
│ │ index.vue
│ └─user
│ index.vue
└─tools
hello.js
打包成SPA
打包成SPA非常简单,默认情况下工程就是打包成SPA的。因此无需做出任何调整,就可以直接将程序打包成SPA。
打包成MPA
1. 打包预期的结果
首先,我们要清楚自己通过修改打包过程,实现怎样的一个目标。显然,我们的目标是非常明确的,我们要将上面的代码,打包到dist目录中。且将pages页面下的每一个index.vue页面都打包出一个相应的 来,并且能够将依赖关系正确的处理。打包出来预期的结果应该是这样的:
│ index.html
├─assets
| ...
└─pages
├─report
│ index.html
├─tdm
│ index.html
└─user
index.html
2. 认识打包过程
为了实现上述的目标,我对rollup的源码进行了深入的研究,并且熟悉了rollup的插件编写。只有对rollup较为深入的理解,才能解决这个问题。rollup的打包过程其实也并不是特别的复杂,或者说最复杂的那一部分暂时不需要去管。rollup打包过程中就是将入口文件找出来,然后将其中的依赖关系都处理好,并生成一个Graph。最后根据这个Graph将html、js等文件的引用关系重新塑造一遍,生成对应的结果并写入到文件当中去。rollup内置了强大的插件功能,在打包每进行到一个新的阶段的时候,rollup就会调用插件中的相关方法来对一些内容进行修饰,进而实现对打包过程的介入并实现更加强大的打包功能。
3. 打包第一步——准备动态生成的代码
我们在打包的过程中,并不会直接用vue文件去生成index.html,这样不仅奇怪,而且会把问题弄得复杂且容易出错。相反,我们会针对每一个需要被打包成页面的index.vue文件,动态的为其生成两个文件。它们分别是index.html和main.js。且这两个文件则都会放在index.vue的同级目录中,如下图所示:
└─src
│ ...
├─pages
│ ├─report
│ │ index.vue
│ │ main.js(动态生成的)
│ │ index.html(动态生成的)
│ ├─tdm
│ │ ...
│ └─user
│ ...
在index.html中,我们先引用了main.js。在main.js中,我们又引用了index.vue,通过这种方式,我们就能够实现将一个index.vue的内容放在一个index.html页面中去展示。从而最终实现一个从index.vue到最终index.html文件。生成的main.js和index.html都是千篇一律的,具体的实现方式如下所示:
function dynamicMainJs() {
return `
import { createApp } from 'vue'
import '/src/style.css'
import App from './index.vue'
createApp(App).mount('#app');
`
}
function dynamicIndexHtml() {
return `
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title>
</head>
<body>
<div id="app">123</div>
<script type="module" src="./main.js"></script>
</body>
</html>
`
}
4. 打包第二步——将要加入打包过程中的动态代码准备好
我们准备好了动态的代码之后,要把动态生成的index.html加入到打包的input当中去。这个较为简单,只要将build.rollupOptions.input配置好即可。下面的三段代码分别是在page页面下检索index.vue、找到需要动态生成文件(index.html、main.js)的目录、将动态生成的index.html、main.js准备好。
检索index.vue
// 找出所有的index.vue
function search_index_vue(dir, index_vues) {
let files = fs.readdirSync(dir);
files.forEach(function (filename) {
const file_absolute_path = path.join(dir, filename);
const stats = fs.statSync(file_absolute_path);
const is_file = stats.isFile();
const is_dir = stats.isDirectory();
const basename = path.basename(filename)
if (is_file && basename == 'index.vue') {
index_vues.push(file_absolute_path);
} else if (basename == 'index.vue') {
console.log('index.vue是专门指向vue的名词,请不要用作文件夹名称');
}
if (is_dir) {
search_index_vue(file_absolute_path, index_vues);
}
})
}
const pages = []
const pages_dir = path.resolve(path.join('src', 'pages'));
const index_vues = [];
// 搜索index.vue
search_index_vue(pages_dir, index_vues);
找到需要动态生成的文件(index.html、main.js)的目录
// 将搜索到的index_vue计算出相对的关系
for (let index_vue of index_vues) {
const dirname = path.dirname(index_vue)
const rel_path = path.relative(src_dir, dirname);
const segs = rel_path.split(path.sep);
const visit_path = segs.join('/');
const page = {}
// 记录下最原始的对应关系
const page_path = path.relative(process.cwd(), path.join(dirname, 'index.html'));
page[visit_path] = '/' + page_path.split(path.sep).join('/');
pages.push(page)
}
准备好动态生成的index.html、main.js:
// 建立好文件到具体目录的映射关系
const idContentMap = new Map();
const input_option = { 'index': './index.html' };
for (const page of pages) {
for (const kv in page) {
// key是js和html文件,value是动态的js和html
idContentMap.set(page[kv], dynamicIndexHtml());
idContentMap.set(page[kv].replace('index.html', 'main.js'), dynamicMainJs());
input_option[kv] = page[kv];
}
}
// 将调整好的input_option参数作为打包参数传递给vite
export default defineConfig({
...
build: {
rollupOptions: {
input: input_option,
...
}
}
})
5. 打包第三步——准备好插件,通过介入打包流程将动态生成的文件加入到打包过程
我们准备写好一个插件,并将这个插件加入到plugins选项当中去。具体的插件方法有三个load(id),resolveId(),generateBundle(),分别解决从内存中加在文本内容、完成相对路径到绝对路径的映射、生成bundle时修改名称这三个问题。
export default function mpa_pack(rawIdContentMap: Map<String, String>): Plugin {
...
return {
name: 'MPA:PACK',
enforce: 'post',
// 读取文件内容
load(id) {
...;
},
// 完成文件id到绝对路径的映射
resolveId(id, importer) {
...
},
// 修改生成文件的路径
generateBundle(outputOptions,bundle){
...
}
}
}
6. 打包第四步——解决动态生成文件到具体路径的转换
rollup打包过程首先会将input里所传的文件,先做一层转换,转换成具体的文件路径名,然后再继续向下处理。虽然我们准备的动态生成的文件不存在于文件系统上的, 但是是有具体路径的,且每一个文件都有具体路径。因此我们需要先解决一下resolveId的方法。当rollup将我们准备的动态文件传递给我们的时候,我们能够将这些文件路径正确的处理好并返回给rollup。resolveId方法就是做这个工作的。
resolveId(id, importer) {
if(id.startsWith("/src/") && id.endsWith("index.html")){
return relative_path_to_abs(id);
}
if(id.startsWith("./") && importer && importer.startsWith(path.join(process.cwd(), "src"))
&& importer.endsWith("index.html")){
const dir = path.dirname(importer);
const main_js_path = dir + path.sep + id.substring(2);
return main_js_path;
}
if(id == `./${index_vue}` && importer && importer.startsWith("/src/") && importer.endsWith("main.js") && rawIdContentMap[importer]){
const relative_path = process.cwd() + importer.split("/").join(path.sep)
return path.dirname(relative_path) + path.sep + index_vue;
}
}
resolveId要解决三个文件从相对路径到具体路径的转换,分别是我们动态生成的index.html、main.js和源代码中的index.vue。index.html是打包时动态生成的文件,它引用了main.js,而main.js又引用了index.vue。因此需要对他们进行转换,由相对路径转换为磁盘上的绝对路径。
7. 打包第五步——让动态生成的文件的读取由从磁盘上改为我们准备好的动态代码
由于我们的动态生成的两组文件,实际上是不存在于磁盘上的,但是我们又给其初始化了具体的路径。因此当rollup拿着我们的文件路径去找对应的文件的时候,注定是找不到的,是会抛出异常的。因此我们需要在打包的过程中,增添一段小的逻辑。如果rollup要加载的文件是我们准备的动态生成的文件,那就直接从内存中取,对于其它文件,则保留原有逻辑。这一小段逻辑也很简单,且rollup也预留了钩子来给我们完成这个事。我们只要在我们编写的插件里,完善一下`load(id)`方法即可。
load(id) {
// 根据id返回具体的js和html文本内容
return absIdContentMap[id];
}
rollup加载文件的方式很简单。首先rollup会加载一系列插件,然后找出这些插件中实现了load(id)方法的。对于load(id)方法,rollup会一个一个的执行,直到某一个方法返回了具体的文件内容为止。因此我们可以做一个拦截,当rollup加载了一系列插件(自然也包含新编写的插件)的时候,一个一个执行load(id)方法时,只要确保我们的方法在遇到动态生成文件的时候,返回动态生成的内容即可。这样rollup拿到文件内容,发现文件内容不为空,就不会再去磁盘上读取了。这样我们就实现了动态文件的加载。
8. 打包第六步——修改动态文件的生成路径
在通常情况下,我们希望生成的打包路径是这样的:
dist/pages/user/index.html
dist/pages/tdm/index.html
dist/pages/report/index.html
dist/index.html
但现实情况是,打包完之后,出现的路径是这样的:
dist/src/pages/user/index.html
dist/src/pages/tdm/index.html
dist/src/pages/report/index.html
dist/index.html
多出了一个src层级,对应的是源码存放的src位置,这样既不美观看起来也不够专业,因此我们需要将前面的src在打包之后去除掉。因此我们需要通过generateBundle方法来实现这个目的:
....
return {
...
enforce: 'post',
generateBundle(outputOptions,bundle){
for(const key in bundle){
if(key.startsWith("src/") && key.endsWith("index.html")){
const value = bundle[key];
const newKey = key.substring(4);
delete bundle[key];
bundle[newKey] = value;
value.fileName = newKey;
}
}
}
}
除了generalBundle方法外,我们还在插件属性中添加了一个enforce属性。这个enforce属性表示我们的插件要后置执行。因为index.html的bundle也是插件生成的,我们要确保我们的插件在生成bundle的插件之后运行。因此需要加上enforce属性。在generateBundle文件中,我们的处理也很简单,看看是不是由我们动态生成的index.html,如果是的话,直接去除掉前面的src/四个字符,这样最后生成的路径就不会包含src了。
9. 打包结果
通过插件完善打包的方法之后,最后的打包效果如下所示:
vite v5.3.3 building for production...
✓ 26 modules transformed.
dist/pages/user/index.html 0.54 kB │ gzip: 0.33 kB
dist/pages/tdm/index.html 0.64 kB │ gzip: 0.36 kB
dist/pages/report/index.html 0.64 kB │ gzip: 0.36 kB
dist/index.html 0.70 kB │ gzip: 0.36 kB
dist/assets/index-BwDQkbRD.css 0.27 kB │ gzip: 0.18 kB
dist/assets/style-CbQMbAXL.css 1.00 kB │ gzip: 0.54 kB
dist/assets/_plugin-vue_export-helper-DxmBaCLa.js 0.09 kB │ gzip: 0.10 kB
dist/assets/pages/user-C9gmg0Ur.js 0.16 kB │ gzip: 0.18 kB
dist/assets/pages/tdm-CcLLPnF2.js 0.20 kB │ gzip: 0.20 kB
dist/assets/pages/report-BcY1BQzc.js 0.20 kB │ gzip: 0.21 kB
dist/assets/index-oLntDkqP.js 2.11 kB │ gzip: 1.09 kB
dist/assets/style-C34ZIGKn.js 53.25 kB │ gzip: 21.85 kB
✓ built in 2.91s
可以看到,打包的结果,达到了我们最初想要的目标。
标签:index,vue,MPA,rollup,js,html,path,打包,页面 From: https://blog.csdn.net/2401_88352022/article/details/143464293