vite + vue3多页面配置记录references,loadEnv等

目的:使用vite创建vue3项目记录细节点

上篇vue2/vue3 + webpack多页面遇到的问题和思考我们使用vue-cli搭建项目都是使用webpack打包的,现在对比一下vite感受一下极速开发体验

增:下一篇vite + vue3 多页面实战优化续集:eslint+lint-staged+husky+stylelint

第一部分:项目基础配置ts相关: lib, references,loadEnv

说到vue3,不得不提ts,说到ts,一定要先到 官方文档了解tsconfig.json配置的意思,这里我觉得有意思的就是references/lib。

我们通过处理一些警告来了解配置:
例如你使用了vue自动导入插件:unplugin-auto-import/vite, 组件自动导入unplugin-vue-components/vite,可能会遇到以下问题

  1. 在自定义ts文件中引入资源,ts无法识别别名@


    ts无法识别别名.png

要在tsconfig.json配置paths

记得将以上自动导入插件生成的文件auto-imports.d.ts,components.d.ts放入到 tsconfig.json的include数组中

// vite.config.ts
import { resolve } from 'path'
export default defineConfig({
  plugins,
resolve: {
  alias: {
    '@': resolve(__dirname, 'src')
  }
}
})
// tsconfig.json
"compilerOptions": {
    "baseUrl": ".",
    "paths":{
      "@": ["src"],
      "@/*": ["src/*"],
    },
 "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts", 
 "components.d.ts"],
  "references": [{ "path": "./tsconfig.node.json" }],
  },
  1. 上面有个references,相当于拆分了tsconfig配置到tsconfig.node.json,关于这个配置我们看官方文档ts3.0新特性:项目引用
自定义文件夹build.png

这个单独拆分出来的配置文件include包含vite.config.ts,说明只是负责编译 vite 的配置文件。
我在根目录新建了一个build文件夹,拆分了plugin的引入和多页面配置,这里红色警告提示要在tsconfig.node.json的include中加入文件


// tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "module": "esnext",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts", "build/*.ts"]
}
  1. src/env.d.ts中我们可以看到有帮助ts识别vue文件的代码,还有三个斜线和reference,这个reference是不是和上面我们tsconfig.json配置文件中的references长的很像
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string
  // 更多环境变量...
}

src/env.d.ts 中的三斜线指令官方文档说明 /// <reference types="vite/client" />
三斜线引用告诉编译器在编译过程中用types形式引入的额外的文件vite/client.d.ts,这个文件里面就是vite帮我们定义的各种常用类型定义,比如css,图片等。下图可以看到又用path的形式引入了./types/importMeta.d.ts,还引入了dom相关声明<reference lib="dom" />
想一想:这个lib在tsconfig.json/compilerOptions配置项目中也有

vite/client.d.ts.png

继续深入到types/importMeta.d.ts发现了可以全局使用的一些声明ImportMetaEnv,GlobOptions

// vite/client/types/importMeta.d.ts
interface ImportMetaEnv {
  [key: string]: any
  BASE_URL: string
  MODE: string
  DEV: boolean
  PROD: boolean
  SSR: boolean
}

综上我们可以发现

  1. vite官方文档环境变量告诉我们在配置多个环境变量的时候可以在src/env.d.ts中再次声明ImportMetaEnv插入自己的全局变量。原来是合并了内部类型,达到新增全局变量的作用。新增环境变量记得VITE_ 开头
// 项目根目录新增的 .env.development文件
VITE_PROJECT_ENV = 'development'
VITE_APP_URL = "https://app-api-0.com/"
  1. <reference lib="dom" /> 中用到的lib,types 和tsconfig.json配置文件中的references/compilerOptions中的"lib": ["esnext", "dom"],types属性一样的,只是不同文件场景不同的使用方式而已。

(1) 在d.ts文件中
声明对某个包的依赖用到了types: /// <reference types="vite/client" />
用路径声明依赖的用到了path: /// <reference path="./types/importMeta.d.ts" />
(2) 在.ts文件里声明一个对@types包的依赖,可以使用–types命令行选项或在tsconfig.json里指定
/// <reference types="node" />

  1. 官方文档-编译选项告诉我们:target 选项用来指定最终代码运行环境所支持的 JavaScrpt 语法的版本; lib 选项的值默认继承自 target:es5,默认注入target值版本es5库和dom 库。所以我们才能在默认项目中看到vite帮我们配置了"lib": ["esnext", "dom"]

  2. 关于配置文件references,我们可以看到在vite项目拆分了出了tsconfig.node.json配置文件,专门用来负责vite.config.ts配置相关编译工作,结构清晰。
    它更重要的作用references官方文档已经告诉我们了: 一套配置,多个工程,修改时只编译子项目本身,显著地加速类型检查和编译。 更细致的举例文章
    所以:拆开后,vite项目中修改vite.config.ts配置并不会导致整个src项目重新编译ts,仅仅只是触发reload

关于环境变量,这里汇总一波
  1. 想在vite.config.ts中使用环境变量,要用vite提供的loadEnv(mode, process.cwd())

单页面应用在src目录下项目中用环境变量可以使用import.meta.env查看.env文件中的自定义环境变量,或者使用import.meta.glob(类似webpack的require.context)读取文件,但是在vite.config.ts配置文件中import.meta是空的

loadEnv的第一参数mode哪里有?两种方式

(1)官网环境变量告诉我们可以直接在vite.config.ts中将defineConfig参数写成函数,函数就有mode参数可用
下面代码中有define,我们在下面讲

export default defineConfig(({ mode }) => {
 // 根据当前工作目录中的 `mode` 加载 .env 文件
return {
    // vite config
    define: {
 // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
        'process.env':  loadEnv(mode, process.cwd(), '')
    }
  }
})

(2)mode值也可以配合scripts命令从process.argv中拿,实际上defineConfig的mode参数也是拿的这个

tips:
a. 注意下面的 --mode development命令在最后面,方便process.argv取其值;
b. 注意 --mode development和新建的.env.development文件同名

// package.json
"scripts": {
    "dev": "vite --host --mode development",
    "test": "vue-tsc --noEmit && vite build --mode test",
    "build": "vue-tsc --noEmit && vite build --mode production",
    "preview": "vite preview"
  }
import { loadEnv } from 'vite'
const mode = process.argv[process.argv.length - 1]
console.log(mode)
const env = loadEnv(mode, process.cwd())
// env
// {
//  VITE_PROJECT_ENV: 'development',
//  VITE_APP_URL: 'https://app-api-0.com/'
// }
  1. 上面代码中提到define,定义全局常量。这在多页面应用中非常有用

在多页面中你会发现:process没有,报错:process is not definedimport.meta.env也没有合并自定义的变量

define: {
 // 设置第三个参数为 '' 来加载所有环境变量,而不管是否有 `VITE_` 前缀。
        'process.env':  loadEnv(mode, process.cwd(), '')
    }

全局使用
const {VITE_APP_URL} = process.env

第二部分:多页面配置

目录结构还是一样的,views下面多个页面都有自己的main.ts/index.vue

├── public
└── build // 多页面生成
└── src
    ├── api  // 请求
    ├── assets // 静态资源
    ├── components  // 公共组件
    ├── config  // 公用类
    └── views  // 页面
         └── home  // 页面
                ├── lang  // 多语言
                ├── icons // svg图标
                ├── components  // 组件
                ├── router  // 路由,选择性需要
                ├── store  // 选择性需要
                ├── main.ts  // 
                ├── index.vue // 
第一步:还是一样新建build文件夹下面有puligns.ts/getPages.ts,我们拆分plugins的引用以及多页面生成代码
//getPages.ts多页面入口html生成

import glob from "glob";
import fs from "fs";
import { resolve } from 'path'

const input = {}
const mainEntry = []
const iconDirs = []
function getPages() {

  // 遍历文件夹中含有main.ts的文件夹路径
  const allEntry = glob.sync("./src/views/**/main.ts");
  // 获取模板
  const temp = fs.readFileSync("./index.html");
  console.log('allEntry', allEntry)
  // 创建多页面模板
  allEntry.forEach((entry: string) => {
    const pathArr = entry.split("/");
    const name = pathArr[pathArr.length - 2];
    // 判断文件是否存在
    try {
      fs.accessSync(`./src/views/${name}.html`);
    } catch (err) {
      console.log(`创建${name}.html文件`);
      const index = temp.toString().indexOf("</body>");
      const content =
        temp.toString().slice(0, index) +
        `<script type="module" src="./${name}/main.ts"></script>` +
        temp.toString().slice(index);
      fs.writeFile(`./src/views/${name}.html`, content, err => {
        if (err) console.log(err);
      });
    }
    // input中的配置
    input[name] = resolve(`src/views/${name}.html`);
    mainEntry.push(resolve(`src/views/${name}/main.ts`))
    iconDirs.push(resolve(process.cwd(), `src/views/${name}/svg`))
  });
};
getPages()
console.log(input, mainEntry, iconDirs)
export {input, mainEntry, iconDirs}

关于路径小记:
1. process.cwd() 永远是项目根目录,也就是package.json那一层
2. __dirname 是当前工作目录,以上就是指build文件夹,例如以下代码在我新建的build文件夹中的getPage.ts

以上代码

  1. 我们以根目录的index.html为模版在src/views目录下生成了input(入口html路径), mainEntry(入口main.ts文件,给VConsole插件用), iconDirs(icon图标,vite-plugin-svg-icons插件要用)

  2. 在每个页面html中插入script,引入自己的ts文件<script type="module" src="./${name}/main.ts"></script>

  3. 记得删除模版,也就是根目录的index.html中的script标签先

我们看打印的效果


input, mainEntry, iconDirs.png

插件代码

import vue from '@vitejs/plugin-vue'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import styleImport, { VantResolve } from 'vite-plugin-style-import';
import {visualizer} from "rollup-plugin-visualizer";
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import viteCompression from "vite-plugin-compression";
import WindiCSS from 'vite-plugin-windicss'
import { viteVConsole } from 'vite-plugin-vconsole';
import {mainEntry, iconDirs} from './getPage'

export const getPlugins = (mode) => {
  console.log('mode', mode)
  return [
    vue(),
    WindiCSS({
      scan: {
        dirs: ['.'], // 当前目录下所有文件
        fileExtensions: ['vue', 'js', 'ts'], // 同时启用扫描vue/js/ts
    }
    }),
    visualizer(),
    AutoImport({
      imports: ['vue']
    }),
    Components(),
    styleImport({
      resolves: [VantResolve()],
    }),
    viteCompression({
      ext: ".gz",
      algorithm: "gzip",
      deleteOriginFile: false
    }),
    viteVConsole({
      entry: mainEntry,
      localEnabled: mode !== 'production',
      enabled: mode !== 'production',
      config: {
        maxLogNumber: 1000,
        theme: 'dark'
      }
    }),
    createSvgIconsPlugin({
      iconDirs,
      symbolId: 'icon-[dir]-[name]'
    })
  ]
}


2022.7.6补充

上面的vite-plugin-style-import 版本低于2.0.0
2.0.0最新的要修改一下,还会提示你没有consla这个依赖,安装一下

import { VantResolve, createStyleImportPlugin } from 'vite-plugin-style-import';

styleImport 没了,换成下面的。
createStyleImportPlugin({
     resolves: [
       VantResolve(),
     ],
     libs: [
       // If you don’t have the resolve you need, you can write it directly in the lib, or you can provide us with PR
       {
         libraryName: 'vant',
         esModule: true,
         resolveStyle: (name) => {
           return `../es/${name}/style`
         },
       },
     ],
   }),
第二步:修改vite.config.ts
import { defineConfig } from 'vite'
import { resolve } from 'path'
import { input } from './build/getPage'
import postCssPxToRem from "postcss-pxtorem"
import { getPlugins } from './build/plugins'
import { loadEnv } from 'vite'

// const mode = process.argv[process.argv.length - 1]

// https://vitejs.dev/config/
export default defineConfig(({mode}) => {
const plugins = getPlugins(mode)
return {
  base: './',
  root: './src/views/',
    publicDir: resolve(__dirname, 'public'),
  plugins,
  define: {
      'process.env': loadEnv(mode, process.cwd())
  },
  css: {
      postcss: {
        plugins: [
          postCssPxToRem({
            rootValue: 37.5, // 1rem的大小
            propList: ['*'], // 需要转换的属性,这里选择全部都进行转换
            selectorBlackList: ['.ig-','.dzg-header-','.za-'], // 忽略的选择器   .ig-  表示 .ig- 开头的都不会转换
          })
        ]
      }
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  build: {
    outDir: '../../dist', // 输出路径
    assetsDir: 'static', // 静态文件目录
        // 默认情况下 若 outDir 在 root 目录下, 则 Vite 会在构建时清空该目录。
    emptyOutDir: true,
    rollupOptions: {
      input,
      output: {
        chunkFileNames: 'static/js/[name]-[hash].js',
        entryFileNames: 'static/js/[name]-[hash].js',
        assetFileNames: 'static/[ext]/[name]-[hash].[ext]',
        manualChunks(id) {
          // 单独打包第三方依赖
          if (id.includes("node_modules")) {
            return id
              .toString()
              .split("node_modules/")[1]
              .split("/")[0]
              .toString();
          }
        }
      }
    }
  }
}
})

有了以上的两步走修改,项目就可以正常跑起来访问了。

配置上,我们重点看几个配置root,publicDir,outDir, rollupOptions /manualChunks

  1. 由于在第一步我们以根目录下的index.html为模板,在src/views下生成了相应页面的html,那么项目root就不是指向根目录了,我们改为新生成的index.html目录 root: './src/views/', 这样启动项目就会自动打开这个新生成的src/views/index.html
    同理:root变了,放静态资源的publicDir也要改一改,否则就获取到不放在public的静态资源了;outDir要改,不然打包生成的dist就跑到views下面了

  2. manualChunks这个就类似我们在webpack中用到的splitchunks,可以单独打包依赖项目。我们利用插件rollup-plugin-visualizer看一看打包后的包分布情况,

未配置manualChunks时看看分布图,以入口页面为维度打包了相应的一个js,我们可以看到被至少两个页面使用的,例如axios,vue-i18n等被打包到了virtual_svg-icons-register-150ef7c7.js ,vue被单独拆出来到windi-79315b5a.js, 这两个命名是随机取的你的依赖名称(真的狗)

未配置manualChunks.png

我们再看配置manualChunks将依赖全部单个打包,每个依赖清晰可见。其中一个叫intlify的包被多个依赖引用,也被单独拆出来了


单独拆包.png

我们也可以根据需要将依赖包打包到一起,例如我将'vue', 'vue-i18n', 'axios'打包到一起名称叫做vandor,将vant单独打包,代码如下:

manualChunks: {
       vandor: ['vue', 'vue-i18n', 'axios'],
       vant: ['vant']
  },
自定义打包.png

我们看分布图有四个大模块(最左侧的一条是其他页面js):

  1. vandor包全部在右侧,包含了我想要的
  2. vant在左下角,符合要求
  3. login页面用到了jsencrypt,所以被打包到了login.js中
  4. md5和svg插件被打包到一起,在名为virtual_svg-icons-register(名称取依赖名命名)的包中

其他小点:

  1. less支持,只需要安装yarn add less -D即可
  2. 项目如果提示没有glob,安装一个glob和@types/glob(类型支持) yarn add glob @types/glob -D
重点关于windicss ,由于多页面更改了root到src/views,要改两个地方,不然不会生效哦

(1)windi.config.ts 也要移动到src/views目录下
(2)plugins中windi.css的扫描范围scan也改一下

WindiCSS({
      scan: {
        dirs: ['.'], // 当前目录下所有文件
        fileExtensions: ['vue', 'js', 'ts'], // 同时启用扫描vue/js/ts
    }
    }),

至于其他,vue-router,pinia,eslint配置这里就不做记录了

以上就是所有实践记录,代码我放到gitee中。vite-vue3-multip-master

在下一篇vite + vue3 多页面实战优化续集:eslint+lint-staged+husky+stylelint中我对项目结构进行了优化,最新的项目结构放在了release分支,可直接用于项目实战vite-vue3-multip-master
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容