首页 > 其他分享 >通过VITE/rollup实现一个工程的代码分别打包成SPA(单页面应用)和MPA(多页面应用)

通过VITE/rollup实现一个工程的代码分别打包成SPA(单页面应用)和MPA(多页面应用)

时间:2024-11-05 20:17:17浏览次数:5  
标签:index vue MPA rollup js html path 打包 页面

问题背景

我们的客户开发的系统会销售给多个不同的单位使用,并且是需要私有化部署的。在有的客户那里,直接部署完就结束了。但是另外一些客户,提出了一些特别的要求。他们要求我们的系统只需要提供一个个功能页面,无需提供菜单管理等功能。功能页面的调度、管理、权限等工作,则是由他们内部的大平台来统一来完成。客户的这个需求比较特别,他们的这种需求,意味着我们必须要把后台做成一个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.htmlmain.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.jsindex.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.htmlmain.js)的目录、将动态生成的index.htmlmain.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.htmlmain.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.htmlmain.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.htmlmain.js和源代码中的index.vueindex.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

相关文章

  • 浏览器中输入URL返回页面过程(超级详细)、DNS域名解析服务,TCP三次握手、四次挥手
    文章目录前言浏览器中输入URL返回页面全过程DNS域名解析过程TCP的三次握手、四次挥手一、浏览器中输入域名二、解析域名2.1具体过程2.2知识补充2.2.1域名体系结构2.2.2查询方式——递归查询、迭代查询2.2.3DNS域名解析过程三、浏览器与目标服务器建立T......
  • aria-hidden属性与页面交互问题
    1.背景与问题1.背景页面中表格有60多条数据,在不做分页处理的情况下,设置表格的最大高度,展示滚动条。2.问题在对前二十条已经展示在页面上的数据进行操作时,没有问题。滚动表格展示出新数据时,对数据进行操作,会有如图报错。并且对于新数据的操作并不生效。2.aria-hidden属性......
  • 六款高颜值注册页面(可复制源码)
    和昨天的一样,带来了六款注册界面,可复制源码(需要定制请加微信)第一款–简约风格HTML<!DOCTYPEhtml><htmllang="zh"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1......
  • 鸿蒙开发进阶(HarmonyOS )加速Web页面的访问
     鸿蒙NEXT开发实战往期必看文章:一分钟了解”纯血版!鸿蒙HarmonyOSNext应用开发!“非常详细的”鸿蒙HarmonyOSNext应用开发学习路线!(从零基础入门到精通)HarmonyOSNEXT应用开发案例实践总结合(持续更新......)HarmonyOSNEXT应用开发性能优化实践总结(持续更新......)当Web页......
  • 【前端】六款高颜值登录页面
    原创吴旭东无限大infinity第一款–简约风格HTML:<!DOCTYPEhtml><htmllang="zh"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width,initial-scale=1.0">......
  • 阶段练习三.新闻页面实现
    <!DOCTYPEhtml><htmllang="en"><head>  <metacharset="UTF-8">  <metaname="viewport"content="width=device-width,initial-scale=1.0">  <title>Document</title>  ......
  • ​Cell Res | 首个知识与数据联合驱动的跨物种生命基础大模型GeneCompass:解析基因调控
    近年来,大语言模型(LLMs)已在自然语言、计算机视觉等通用领域引发了新一轮技术革命,通过大规模语料和模型参数进行预训练,LLMs能够掌握语言的共性规律,能够对多种下游任务产生质的提升,已经形成了新的人工智能范式。在生命科学领域,单细胞组学技术的突破产生了大量不同物种细胞的基因表达......
  • ArkTS鸿蒙页面(ArkUI-X Empty Ability)
    1.基础1.1.存储变量,常量lettitle:string='巨无霸汉堡'console.log('字符串title',title)//1.2数字number类型letage:number=18console.log('年纪age',age)//1.3布尔boolean类型(true真,false假)letisLogin:boolean=falseconsole.log(&#......
  • 别闹了,让开发来设计B端页面,哪还有法看,都长一个样。
    一、引言在当今数字化时代,B端(企业端)软件的重要性日益凸显。一个高效、美观且易用的B端页面对于企业的业务运营和用户体验至关重要。然而,当开发人员被赋予设计B端页面的任务时,往往会引发争议。许多人认为开发人员设计的B端页面“哪还有法看”,且“都长一个样”。这种现......