项目背景:有多个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 create vite\@latest vite-multi npm create vite\@latest vite-multi yarn create vite vite-multi 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 }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' , 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 ) 方法用于将文件从源路径同步复制到目标路径。
package.json 文件中添加以下指令,以执行创建子页面的脚本;
1 2 3 4 "scripts" : { ... "new:page" : "node ./scripts/index.mjs" }
然后开发写创建子页面的脚本,第一步要先提示用户输入要创建的页面名称和描述,并验证输入的格式;
1 2 3 4 5 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 目录下' )
使用 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 ('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 ] || inputNameconst 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 } })
在同级 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 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) } })function setFile (datas ) { fs.writeFile ( path.resolve ('./scripts' , 'multiPages.json' ), JSON .stringify (datas), 'utf-8' , (err ) => { if (err) throw err 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 () })
将新页面的信息写入 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 ) 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:支持单页面打包和打包全部页面 处理方式:
多页面入口配置 使用说明: 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 const project = require ('./scripts/multiPages.json' )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), ...
项目根路径修改为 root: ‘./src/projects/‘ ,不修改其实也能实现,但是不改的话打包后dist文件夹层级太深;
修改打包的输出路径为:
1 2 3 build : { outDir : path.resolve (__dirname, `dist` ), ...
修改 envDir
配置项,修改到项目根的路径,用于加载.env
环境变量文件
1 envDir : path.resolve (__dirname)
情况2:只支持单页面打包
多页面入口配置
使用说明: 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 import chalk from 'chalk' const project = require ('./scripts/multiPages.json' )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 root : path.resolve (__dirname, `./src/Project/${npm_config_page} ` )
修改打包的输出路径为:
1 2 3 build : { outDir : path.resolve (__dirname, `dist/${ npm_config_page } ` ) ...
修改 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 , emptyOutDir : true , terserOptions : { compress : { keep_infinity : true , drop_console : true , drop_debugger : true , }, 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 (); } } } } ...
十、 完善项目架构
增加 eslint
增加 prettier
增加git提交 husky + lint-staged + Commitlint 的校验
配置环境变量
引入 element-plus UI库并配置自动引入等
这些就不赘述了,不清楚的可以直接去我项目里拉代码
⏰ 写在最后
在搭建的过程中,一定要注意因为修改了项目根目录(root)所带来的问题,利用 path.resolve(__dirname, …) 将路径重置到相对于项目根的位置!
关于如何使用这个多页面脚手架,可以看 README.md 文件。
项目源代码放在这了 github源码 ,对你有帮助请给个🌟,感谢🙏