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 defined,import.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閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容