vue3+vite5 构建多页面多项目开发及打包

项目背景:有多个UI及布局都相同的项目,但是拆成多个项目开发太乱后期不好维护。所以想把多个项目整个到一个项目中,抽离公共的组件,方法等。

基于技术栈:vue3 + vite5 + element-plus+ esint + prettier + stylelint + husky + lint-stage + commitlint

1. 项目目标

  • 🍀 支持打包指定子页面,打包后的文件夹:各页面相互独立(各子页面解耦,避免相互影响)
  • 🍀 支持启动指定子页面(常规的多页面项目,启动后需要手动拼接页面地址,或者在根目录做一个重定向的页面,总之调试非常不便)
  • 🍀 支持指令化新建页面(手动创建页面太麻烦,每次都得复制一份干净的文件夹)
  • 🍀 自由选择创建ts页面 / js页面(对于一些重要的页面可以使用ts提高规范性,一些简单的页面则使用js提高开发效率)

2. 本文将从以下几个方面逐步讲解:

  • 项目目录结构
  • 新建项目
  • 安装依赖及一些基础插件
  • vite配置项修改
  • ts配置
  • 多页面入口配置
  • 多页面打包配置
  • 指令化新建子页面(*重点)
  • 多页面架构改造(*重点)
  • 完善项目架构

教程

一、 项目目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
├── README.md 
├── .husky //git hook钩子
│ ├── commit-msg //规范 commit message 信息
│ └── verify-commit-msg.mjs //脚本:commitlint 替代方案
├── dist //打包输出目录
├── scripts //存放一些脚本
│ ├── template //创建子页面的js模版
│ ├── template-ts //创建子页面的ts模版
│ ├── index.mjs //创建子页面的脚本
│ └── multiPages.json //子页面描述说明集合文件
├── src
│ ├── arrets //公共静态资源
│ ├── components //公共组件
│ ├── store //pinia 共享状态存储库
│ ├── utils //公共方法
│ └── Projects //多页面文件夹
├── types //ts 声明文件
├── .env.development //开发环境-环境变量
├── .env.production //生产环境-环境变量
├── .eslintrc.cjs //eslint 配置
├── .gitignore //git 提交忽略文件
├── .prettierignore //prettier 忽略文件
├── .prettierrc.js //prettier 配置
├── .stylelintignore //stylelint 忽略文件
├── .stylelintrc.js //stylelint 配置
├── .pnpm-lock.yaml //锁定项目于一份各个依赖稳定的版本信息
├── .stats.html //chunck size 分析页面
├── tsconfig.json //ts 配置
├── tsconfig.node.json //vite在node环境中的 ts 规则
├── vite.config.ts //vite 配置
├── package.json

二、 新建项目

首先我们用命令行新建一个vite项目,不要使用模板创建,就创建一个基础模板就行,创建命令如下:

1
2
3
4
5
6
7
8
9
10
11
# npm 6.x
npm create vite\@latest vite-multi

# npm 7+, extra double-dash is needed:
npm create vite\@latest vite-multi

# yarn
yarn create vite vite-multi

# pnpm
pnpm create vite vite-multi

三、 安装依赖及一些基础插件

新建项目后记得 npm i 安装依赖。然后我们先装一些基础的插件,例如vue-router等,方便后面调试, 这里可能没装全,大家根据报错提示自行安装即可

1
2
3
4
5
6
7
8
9
10
11
//安装vue-router4
npm install vue-router\@next -S

//安装 sass
npm install sass -D

//安装 chalk(chalk是一个颜色的插件。可以通过chalk.blue(‘hello world’)来改变console打印的颜色)
npm install chalk@^4.1.2

//处理使用 node 模块代码飘红,例如 ‘找不到模块 “path“ 或其相对应的类型声明’
npm install @types/node --save-dev

四、 vite配置项修改

vite.config.ts 进行调整,先做一些基础的配置,后面我们调通项目之后再丰富项目插件。

🍀 配置文件路径的别名,方便书写文件引入路径。

1
2
3
4
5
6
resolve: {
alias: {
'@': path.join(__dirname, './src'),
'@Project': path.join(__dirname, './src/Project')
}
}

五、 ts配置

项目根目录下找到 tsconfig.json 文件,它是是用来配置 TS 编译选项的。
以下是我使用的配置项,基本对每一项都做了解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
"compilerOptions": {
"target": "esnext", //用于指定 TS 最后编译出来的 ES 版本
"types": ["vite/client"], //要包含的类型声明文件名列表
"useDefineForClassFields": true, //将 class 声明中的字段语义从 [[Set]] 变更到 [[Define]]
"module": "esnext", // 设置编译后代码使用的模块化系统:commonjs | UMD | AMD | ES2020 | ESNext | System
"moduleResolution": "node", // 模块解析策略,ts默认用node的解析策略,即相对的方式导入
"strict": true, //开启所有的严格检查
"jsx": "preserve", //在 `.tsx`文件里支持JSX: `"React"`或 `"Preserve"`
"sourceMap": false, // 生成目标文件的sourceMap文件
"resolveJsonModule": true, //允许导入扩展名为“.json”的模块
"isolatedModules": true, //确保每个文件都可以在不依赖其他导入的情况下安全地进行传输
"esModuleInterop": true, //支持导入 CommonJs 模块
"lib": ["esnext", "dom", "ES2015"], //TS需要引用的库,即声明文件,es5 默认引用dom、es5、scripthost,如需要使用es的高级版本特性,通常都需要配置,如es8的数组新特性需要引入"ES2019.Array",
// "noLib": false, //不包含默认的库文件( lib.d.ts)
"skipLibCheck": true, //忽略所有的声明文件( *.d.ts)的类型检查
"allowJs": true, // 允许编译器编译JS,JSX文件
"noEmit": true, // 不输出文件,即编译后不会生成任何js文件
"allowSyntheticDefaultImports": true, //允许从没有设置默认导出的模块中默认导入。这并不影响代码的输出,仅为了类型检查。默认值:module === "system" 或设置了 --esModuleInterop 且 module 不为 es2015 / esnext
"baseUrl": "./", 解析非相对模块的基地址,默认是当前目录
"paths": {
"@/*": ["src/*"], //解决引入报错 找不到模块“@/xxxx” 或其相应的类型声明
"@Project": ["src/Project/*"]
}
},
"include": [
"scripts/**/*.ts",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"scripts/index.mts",
"scripts/template-ts/router/routes.ts",
"scripts/template-ts/router/index.ts",
"scripts/template-ts/main.ts",
"src/env.d.ts",
"src/global.d.ts"
],
"exclude": ["vite.config.ts"],
"references": [{ "path": "./tsconfig.node.json" }] //每个引用的`path`属性都可以指向到包含`tsconfig.json`文件的目录,或者直接指向到配置文件本身(名字是任意的)
}

六、 多页面入口配置

vite 使用的是 rollup 的打包方式。

1. 基本配置

想要将项目改造成多页面项目,我们可以自定义底层的 Rollup 打包配置,只需要指定多个 .html 文件作为入口点即可,此设置在 build.rollupOptions.input 配置项下。

首先我们现在 projects 文件夹下新建两个子页面 page1、page2 ,目录结构如下:

然后在 vite.config.ts 文件中指定这两个子页面的入口。

1
2
3
4
5
6
7
8
build: {
rollupOptions: { //自定义底层的 Rollup 打包配置
input: {
project1: resolve(__dirname, 'src/projects/page1/index.html'),
project2: resolve(__dirname, 'src/projects/page2/index.html')
}
}
}

这里需要注意的是:
__dirname 占位符指的是 vite.config.js 文件所在的目录,即使修改了项目的根目录,它的值也不会变(后面我们将会修改项目的根目录)。

2. 动态生成多页面入口

因为我们要不断新建子页面,不可能每个子页面都手动去配置入口,所以可以获取到 /projects 文件夹下文件名后,动态配置多页面入口。

fs 模块是 Node.js 官方提供的、用来操作文件的模块。它提供了一系列的方法和属性,用来满足用户对文件的操作需求,这里我们用到了 fs.readdirSync 方法。
fs.readdirSync 方法同步返回一个包含“指定目录下所有文件的名称”的数组对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import fs from "fs";

function getEntryPath () {
const map = {} //最后生成的多页面配置项
const PAGE_PATH = path.resolve(\__dirname, './src/Project') //指定要查询的目录
const entryFiles = fs.readdirSync(PAGE_PATH) //获取到指定目录下的所有文件名
entryFiles.forEach(filePath => { //遍历处理每个子页面的入口
map[filePath] = path.resolve(__dirname,
`src/Project/${filePath}/index.html`
)
})
return map
}

// 自定义底层的 Rollup 打包配置
rollupOptions: {
input: getEntryPath()
}

3. 配置重定向页面

配置完多页面入口后,我们可以启动项目看看效果。 npm run dev 启动项目:

你会发现什么都没有,因为此时项目根路径还是‘/’,找不到可以作为入口的index.html文件,这时我们只能手动拼接上地址 /src/projects/page1/ ,进入子项目的 index.html 文件

修改项目根目录下的 index.html(与vite.config.js同级)

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>重定向</title>
</head>
<body>
<p><a href="./src/projects/page1/index.html">子页面1</a></p>
<p><a href="./src/projects/page2/index.html">子页面2</a></p>
<script>
</script>
</body>
</html>

七、 多页面打包配置

build 选项指定多个入口之后,就可以进行多页面打包了。我们执行 npm run build 看看打包生成的 dist 文件夹结构(这是完成版的截图,初步配置打包不区分 js/css/png等文件夹)。

1
2
3
4
5
6
7
8
9
10
build: {
rollupOptions: {
input: 指定多页面入口,
output: {
assetFileNames: '[ext]/[name]-[hash].[ext]', //静态文件输出的文件夹名称
chunkFileNames: 'js/[name]-[hash].js', //chunk包输出的文件夹名称
entryFileNames: 'js/[name]-[hash].js', //入口文件输出的文件夹名称
}
}
}

占位符说明 >>>

[extname] :文件扩展名,包括前面的 . ,例如 .css
[ext] :文件扩展名,不包括前面的 . ,例如 css
[name] :文件名;
[hash] :基于文件内容生成的哈希值,可以通过 [hash:10] 设置特定的哈希长度;

八、 指令化新建子页面( 重点来了~

到这里其实已经改造出来了一个多页面项目脚手架。但是离我们都目标还相差甚远:

  • 🍀 不能指令化创建页面
  • 🍀 不能单独启动指定的子项目
  • 🍀 打包后所有子项目的静态文件都混淆在一起

那么怎么解决这些问题呢? 我们逐个剖析:

先来解决指令化新建页面的问题,既然要创建页面,就是要和文件打交道,所以我们还是要使用到 fs 文件系统模块。先来了解它的一下几个方法,我们之后会用到(这里只是大概对方法简单说明,具体使用自行查询):

🔥 fs.mkdirSync( path, options ) 方法用于同步创建目录,创建子页面主要就是使用这个方法。

🔥 fs.readFile( filename, encoding, callback_function ) 方法用于异步读取指定文件中的内容。

🔥 fs.writeFile( file, data, options, callback ) 方法用于异步读取指定文件中的内容。

🔥 fs.existsSync( path ) 方法用于同步检测目录是否存在;

🔥 fs.readdirSync( path, options ) 方法用于同步读取给定目录的内容。该方法返回一个数组,其中包含目录中的所有文件名或对象。

🔥 fs.copyFileSync( src, dest, mode ) 方法用于将文件从源路径同步复制到目标路径。

  1. package.json 文件中添加以下指令,以执行创建子页面的脚本;
1
2
3
4
"scripts": {
...
"new:page": "node ./scripts/index.mjs"
}
  1. 然后开发写创建子页面的脚本,第一步要先提示用户输入要创建的页面名称和描述,并验证输入的格式;
1
2
3
4
5
//./scripts/index.mjs
const log = (message) => console.log(chalk.green(`${message}`))
const successLog = (message) => console.log(chalk.blue(`${message}`))
const errorLog = (error) => console.log(chalk.red(`${error}`))
log('请输入要生成的"页面名称:页面描述"、会生成在 /src/projects 目录下')
  1. 使用 fs.existsSync 方法验证是否已存在同名页面;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//process.stdin属性是流程模块的内置应用程序编程接口,用于侦听用户输入,它使用on()函数来监听事件。
process.stdin.on('data', async (chunk) => {
// 获取输入的信息
const content = String(chunk).trim().toString()
const inputSearch = content.search(':')
if (inputSearch == -1) {
errorLog('格式错误,请重新输入')
return
}
// 拆分用户输入的名称和描述
const inputName = content.split(':')[0]
const inputDesc = content.split(':')[1] || inputName
const isTs = process.env.npm_config_ts
successLog(`将在 /src/Project 目录下创建 ${inputName} 文件夹`)
const targetPath = resolve('./src/Project', inputName)
// 判断同名文件夹是否存在
const pageExists = fs.existsSync(targetPath)
if (pageExists) {
errorLog('页面已经存在,请重新输入')
return
}
})
  1. 在同级 script 文件夹下,新建 multiPages.json 用于记录目前已有的页面名称,每次新建页面都会写入进去;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 获取multiPages.json文件内容,获取当前已有的页面集合
await fs.readFile(
path.resolve('./scripts', 'multiPages.json'),
'utf-8',
(err, data) => {
//获取老数据
let datas = JSON.parse(data)
//和老数据去重
let index = datas.findIndex((ele) => {
return ele.chunk == inputName
})
if (index == -1) {
//写入新页面的
let obj = {
chunk: inputName,
chunkName: inputDesc
}
datas.push(obj)
setFile(datas)
}
})

// 写入multiPages.json
function setFile(datas) {
// 通过writeFile改变数据内容
fs.writeFile(
path.resolve('./scripts', 'multiPages.json'),
JSON.stringify(datas),
'utf-8',
(err) => {
if (err) throw err
// 在project中建立新的目录
fs.mkdirSync(targetPath)
const sourcePath = resolve(
isTs ? './scripts/template-ts' : './scripts/template'
)
copyFile(sourcePath, targetPath)
process.stdin.emit('end')
}
)
}

...

process.stdin.on('end', () => {
console.log('exit')
process.exit()
})
  1. 将新页面的信息写入 multiPages.json 文件后,在 projects 文件夹下复制我们提前创建好的模板页面。我这里创建了两个模版页面,分别是js和ts的模版,如果需要创建支持 TS 的子页面,创建命令为 npm run new:page –ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 判断文件夹是否存在,不存在创建一个
const isExist = (path) => {
if (!fs.existsSync(path)) {
fs.mkdirSync(path)
}
}
//递归复制模版文件夹内的文件
const copyFile = (sourcePath, targetPath) => {
const sourceFile = fs.readdirSync(sourcePath, { withFileTypes: true })

sourceFile.forEach((file) => {
const newSourcePath = path.resolve(sourcePath, file.name)
const newTargetPath = path.resolve(targetPath, file.name)
//isDirectory() 判断这个文件是否是文件夹,是就继续递归复制其内容
if (file.isDirectory()) {
isExist(newTargetPath)
copyFile(newSourcePath, newTargetPath)
} else {
fs.copyFileSync(newSourcePath, newTargetPath)
}
})
}

九、 多页面架构改造

指定子页面可以在执行 npm run dev 命令时配置自定义环境变量。格式为 npm run dev –变量名=值 ,例如我要单独启动 pageone 子页面: npm run dev –page=page1 。关于npm环境变量的使用可以查看传送门
如何启动后直接进入这个子页面呢?这时候我们就需要修改项目的根路径了:

1
2
3
注意:将根路径修改到指定子页面目录下后,就只能单独启动/打包 指定的子页面了,无法打包全部的子页面。这么做有以下优缺点:
优点:打包后的子页面相互独立,可以直接启动指定的子页面;
缺点:无法一次性打包全部页面,需要配置重定向页面方便调试

所以你可以根据需要决定是否要修改项目根目录,因为我考虑到页面是逐步新增的,需要一次性打包全部页面的情况很少,而且我认为打包后的子页面相互独立非常重要,避免出现一些意料之外的问题影响多个页面,所以我果断选择了每次都单独打包页面。

这里我把两种情况的处理都列出来:

情况1:支持单页面打包和打包全部页面

处理方式:

  1. 多页面入口配置
    使用说明: npm run build 打包全部页面; npm run build –page=页面名称 打包单页面;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//vite.config.ts

// 引入多页面配置文件
const project = require('./scripts/multiPages.json')
// 获取npm run dev后缀 配置的环境变量
const npm_config_page:string = process.env.npm_config_page || ''

let filterProjects = []
if (npm_config_page) {
//如果指定了单页面打包,过滤出这个页面的配置项
filterProjects = project.filter((ele) => {
return ele.chunk.toLowerCase() === npm_config_page.toLowerCase()
})
console.log(`--------单独构建:${filterProjects[0]['chunkName']}--------`)
} else {
filterProjects = project
}

//多页面入口
const getEnterPages = (p) => {
const pages = {}
p.forEach((ele) => {
const htmlUrl = path.resolve(
__dirname,
`src/projects/${ele.chunk}/index.html`
)
pages[ele.chunk] = htmlUrl
})
return pages
}

//入口配置
build: {
rollupOptions: {
input: getEnterPages(filterProjects),
...
  1. 项目根路径修改为 root: ‘./src/projects/‘ ,不修改其实也能实现,但是不改的话打包后dist文件夹层级太深;

  2. 修改打包的输出路径为:

1
2
3
build: {
outDir: path.resolve(__dirname, `dist`), // 指定输出路径
...
  1. 修改 envDir 配置项,修改到项目根的路径,用于加载.env 环境变量文件
1
envDir: path.resolve(__dirname)

情况2:只支持单页面打包

  1. 多页面入口配置

使用说明: npm run build –page=页面名称 单独打包指定页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//vite.config.ts
import chalk from 'chalk'

// 引入多页面配置文件
const project = require('./scripts/multiPages.json')
// 获取npm run dev后缀 配置的环境变量
const npm_config_page:string = process.env.npm_config_page || ''
// 命令行报错提示
const errorLog = (error) => console.log(chalk.red(`${error}`))

//获取指定的单页面入口
const getEnterPages = () => {
if (!npm_config_page) errorLog('-------------------请在命令行后以 `--page=页面名称` 格式指定页面名称!-------------------')
const filterArr = project.filter(item => item.chunk.toLowerCase() == npm_config_page.toLowerCase())
if (!filterArr.length)
errorLog('-------------------不存在此页面,请检查页面名称!-------------------')

return {
[npm_config_page] : path.resolve(
__dirname,
`src/Project/${npm_config_page}/index.html`
)
}
}

//入口配置
build: {
rollupOptions: {
input: getEnterPages(),
...

  1. 项目根路径修改到用户输入的单页面文件夹下
1
root: path.resolve(__dirname, `./src/Project/${npm_config_page}`)
  1. 修改打包的输出路径为:
1
2
3
build: {
outDir: path.resolve(__dirname, `dist/${ npm_config_page }`) // 指定输出路径
...
  1. 修改 envDir 配置项,修改到项目根的路径,用于加载.env 环境变量文件
1
envDir: path.resolve(__dirname)

到这里,多页面架构已经基本改造完成了🎉🎉🎉

再做一些打包的优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
build: {
assetsInlineLimit: 4096, //小于此阈值的导入或引用资源将内联为 base64 编码,以避免额外的 http 请求
emptyOutDir: true, //Vite 会在构建时清空该目录
terserOptions: {
compress: {
keep_infinity: true, // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题
drop_console: true, // 生产环境去除 console
drop_debugger: true, // 生产环境去除 debugger
},
format: {
comments: false, // 删除注释
},
},
rollupOptions: {
input: ...
output: {
...
compact: true, //压缩代码,删除换行符等
manualChunks: (id: string) => { //配置分包
if(id.includes("node_modules")) {
return id.toString().split('node_modules/')[1].split('/')[0].toString(); // 拆分多个vendors
}
}
}
}
...

十、 完善项目架构

  1. 增加 eslint
  2. 增加 prettier
  3. 增加git提交 husky + lint-staged + Commitlint 的校验
  4. 配置环境变量
  5. 引入 element-plus UI库并配置自动引入等

这些就不赘述了,不清楚的可以直接去我项目里拉代码

⏰ 写在最后

在搭建的过程中,一定要注意因为修改了项目根目录(root)所带来的问题,利用 path.resolve(__dirname, …) 将路径重置到相对于项目根的位置!

关于如何使用这个多页面脚手架,可以看 README.md 文件。

项目源代码放在这了 github源码,对你有帮助请给个🌟,感谢🙏


vue3+vite5 构建多页面多项目开发及打包
http://example.com/2024/01/23/vite-multi-project/
作者
Shber
发布于
2024年1月23日
许可协议