美文网首页react & vue & angularvue
vite + vue3多页面配置记录references,loa

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

作者: 云鹤道人张业斌 | 来源:发表于2022-05-26 17:35 被阅读0次

    目的:使用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

    相关文章

      网友评论

        本文标题:vite + vue3多页面配置记录references,loa

        本文链接:https://www.haomeiwen.com/subject/aoqfprtx.html