全局方法挂载和类型声明

全局挂载还是很简单的,照着官方的例子写就可以了,我的做法是创建了一个单独ts文件,通过app.use()的方式来进行批量挂载,代码如下:

import type { App } from "vue";

/**
 * @description: 获取assets文件
 * @param {string} path
 * @Date: 2023-05-05 10:05:18
 * @Author: mulingyuer
 */
export function getAssetsFile(path: string): string {
    return new URL(`../assets/${path}`, import.meta.url).href;
}

export default {
    install(app: App) {
        app.config.globalProperties[`$getAssetsFile`] = getAssetsFile;
        app.provide(`$getAssetsFile`, getAssetsFile);
    }
};

除了通过globalProperties的方式注册在全局上,我还通过provide注入的方式挂载,这个注入可比globalProperties省事的多了,但是它的类型推断最后是unknown,还得自个声明一下类型,就又给人一种鸡肋的感觉。

最后在main.ts中use一下

import { createApp } from "vue";
import App from "./App.vue";

//全局方法
import globalMethods from "./utils/globalMethods";


const app = createApp(App);
app.use(globalMethods);

app.mount("#app");

这就完事了,使用的时候如下:

<script lang="ts">
import { defineComponent, inject, getCurrentInstance } from "vue";

export default defineComponent({
    setup() {
        const fns = inject("$getAssetsFile");
        console.log(fns);

        const fnd = getCurrentInstance()!.proxy!.$getAssetsFile;
        console.log(fnd);

        return {};
    }
});
</script>

直接通过inject接,或者通过getCurrentInstance获取,各有各的恶心人的地方:

inject:

获取的内容类型是unknown,使用时需要自己把类型贴上,比如:const fns = inject<Function>("$getAssetsFile");

为了更加准确的推导,你可能还需要从别处引入类型声明,我都从别处引入类型声明了,我还用个勾八全局方法,都是import,我直接import引入对应的方法不是更加省事了。

getCurrentInstance:

这个玩意也很膈应人,getCurrentInstance()返回值可能是null,它的proxy属性也可能null,所以不得不加!声明存在,然后再通过属性获取到挂载的方法。

使用这种方式的优势在于如果我们事先声明了类型,那么在这就能直接推导出来,恶心的点也在这,新版的类型声明又特么变了,具体如下:

创建一个global.d.ts文件,global名字自己随意,你也可以起个vue.d.ts文件。

import { ComponentCustomProperties } from "vue";
import { getAssetsFile } from "../utils/globalMethods";

declare module "vue" {
    export interface ComponentCustomProperties {
        $getAssetsFile: typeof getAssetsFile;
    }
}

export default ComponentCustomProperties;

注意:务必默认导出ComponentCustomProperties

否则ts类型各种报错,这个问题之前是没有的。

vw的转换相关配置

这次是新项目,所以我才用了现在比较新的vw转换,用于代替rem转换,他有一个优势:

  1. 不需要单独的js脚本去更改根元素字体大小

由于不需要单独的js脚本,对于spa做ssr还是很省事的,就不需要单独把这个rem脚本提取出来,然后在index.html中引入了。

vw目前业界支持度还是很高的,如果不是支持远古设备,值得体验。

vw的原理和rem差不多,只不过现在是相对于视口宽度vw来做了。

安装插件:

pnpm i postcss-px-to-viewport-8-plugin autoprefixer -D

postcss-px-to-viewport-8-plugin做vw转换,autoprefixer 做浏览器前缀添加。

vite内置了postcss,所以不需要我们单独安装,直接在项目目录与src目录同级,创建postcss.config.js文件,填入以下内容:

module.exports = {
    plugins: {
        "postcss-px-to-viewport-8-plugin": {
            viewportWidth: 375,
            unitPrecision: 6,
            unitToConvert: "px",
            propList: ["*"],
            selectorBlackList: [],
            minPixelValue: 2
        },
        autoprefixer: {}
    }
};
  • viewportWidth 表示的是设计稿的宽度
  • unitPrecision 表示转换后保留几位小数位
  • unitToConvert 需要被转换的单位是什么
  • propList 需要转换的css属性,比如font-size这些,星号表示所有的都需要
  • selectorBlackList 指定不需要进行转换的class选择器
  • minPixelValue 最小转换的值,最小2是可以转换的,1不行。

本来plugins应该是最新是以数组形式使用的

const postcssPxToViewport8Plugin = require("postcss-px-to-viewport-8-plugin");
const autoprefixer = require("autoprefixer");

module.exports = {
    plugins: [
        postcssPxToViewport8Plugin({
            viewportWidth: 375,
            unitPrecision: 6,
            unitToConvert: "px",
            propList: ["*"],
            selectorBlackList: [],
            minPixelValue: 2
        }),
        autoprefixer
    ]
};

但是感觉略有麻烦,看大家自己选择了。

配置完毕后我们还需要创建一个.browserslistrc用来表面需要兼容的浏览器有哪些,这个文件和postcss.config.js同级

.browserslistrc

> 1%
last 2 versions
not ie 11

具体的可以看看我之前那篇 浏览器前缀 的文章,里面有具体讲过这个文件的配置意思。

到这基本就完事了,启动项目即可享受。

varlet框架的自动引入

github地址:varlet

这是一个vue3的移动端Material design风格框架,简单用用还是可以,如果是复杂业务考虑好再用。

框架本身就有对这个做支持,我们只需要安装一下插件:

pnpm i @varlet/ui unplugin-vue-components unplugin-auto-import -D

由于使用自动引入,所以务必不要在main.ts中引入注册了,不需要,否则你打包完就是全量引入,错误示范如下:

import App from './App.vue'
import Varlet from '@varlet/ui'
import { createApp } from 'vue'
import '@varlet/ui/es/style'

createApp(App).use(Varlet).mount('#app')

vite.config.ts配置自动引入:

import vue from '@vitejs/plugin-vue'
import components from 'unplugin-vue-components/vite'
import autoImport from 'unplugin-auto-import/vite'
import { VarletUIResolver } from 'unplugin-vue-components/resolvers'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    vue(),
    components({
      resolvers: [VarletUIResolver()]
    }),
    autoImport({
      resolvers: [VarletUIResolver({ autoImport: true })]
    })
  ]
})

最后会在第一次启动的时候生成:auto-imports.d.tscomponents.d.ts文件,建议这个也上传到git仓库,省的每次都生成,在大量代码的情况下可能会卡顿。

tsconfig.json中还需要添加一下这两个文件:

{
  "include": ["auto-imports.d.ts", "components.d.ts"]
}

至此完成。

环境变量和对应的类型声明

使用环境变量是项目完整度一个离不开的东西,你想想你在开发环境api可能是局域网提供的,正式版又是另一个地方提供的,通过环境变量进行配置可以节省很多重复的操作。

vite使用指定的环境变量和webpack一样,都是通过--model [环境变量文件名] 实现

vite在初始化的时候就已经创建了一个vite-env.d.ts文件,我们直接用它这个,没有就自己手动创建。

/// <reference types="vite/client" />

interface ImportMetaEnv {
    readonly VITE_APP_NAME: string;
    readonly VITE_ENV: string;
    readonly VITE_OPEN_ANALYZER: string;
}

interface ImportMeta {
    readonly env: ImportMetaEnv;
}

这次的环境变量类型声明和之前略有不同,这也是难受之所在,老特么换,唉。

实现在vite.config.ts中获取到环境变量

咋一看好像有问题,其实在vite.config.ts中通过process.env拿到的只是node的环境变量,而vite提供的import.meta.env这种方式只能在项目文件中使用,比如main.ts,是无法在vite本身的配置文件中使用的,我们只能通过其他方式实现:

import { defineConfig, loadEnv } from "vite";

export default ({ mode }) => {
  process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };

  return defineConfig({...});
}

这样我们就能通过 process.env 拿到当前启动时所使用的环境变量了。

利用环境变量实现正式打包gzip

安装一下gizp插件:

pnpm i vite-plugin-compression -D

创建两个环境变量文件,于src目录同级,填入对应的内容:

.env.development

VITE_ENV="development"

.env.production

VITE_ENV="production"

vite.config.ts文件添加

import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import viteCompression from "vite-plugin-compression";

// https://vitejs.dev/config/
export default ({ mode }) => {
    process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };

    //插件
    const plugins = [
        vue(),
    ];
    //gzip压缩
    if (process.env.VITE_ENV === "production") {
        plugins.push(
            viteCompression({
                verbose: true,
                disable: false,
                threshold: 10240,
                algorithm: "gzip",
                ext: ".gz"
            })
        );
    }
    
    return defineConfig({
        plugins,
    });
};

通过这种方式我们还可以实现一个功能,就是代码视图分析。

利用环境变量实现代码视图分析

vue cli 提供了report修饰符

vue-cli-service build --report

这样就能生成一个打包后代码分析网页report.html用于帮助我们更好的处理打包后的代码处理。

但是vite自身没有这个功能,我们的目标是和vue cli类似,通过一个命令实现这个功能。

首先我们创建一个专门的环境变量文件:

.env.analyzer

VITE_ENV="production"
VITE_OPEN_ANALYZER="true"

通过这个VITE_OPEN_ANALYZER我们来标识是否需要开启代码分析。

安装依赖:

pnpm i rollup-plugin-visualizer -D

配置vite.config.ts文件

import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { visualizer } from "rollup-plugin-visualizer";


// https://vitejs.dev/config/
export default ({ mode }) => {
    process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };

    //插件
    const plugins = [
        vue(),
    ];
    //代码分析
    if (process.env.VITE_OPEN_ANALYZER && process.env.VITE_OPEN_ANALYZER === "true") {
        plugins.push(
            visualizer({
                emitFile: true,
                filename: "analyzer.html",
                gzipSize: true
            })
        );
    }
    
    return defineConfig({
        plugins,
    });
};

配置完成后我们给package.json文件配置一个scripts脚本命令:

{
  "scripts": {
    "analyzer": "vue-tsc && vite build --mode analyzer"
  }
}

使用的时候运行analyzer即可。

pnpm run analyzer

这个插件缺陷也很大,样式丑,而且分析的文件大小居然是没有打包前的文件大小,我build之后,js文件可能就80kb,这个分析的文件,他把没有压缩的包体给你计算出来了,巨难受,只能用来看看是不是有多余的引入之类的。

Prettier - Code formatter

推荐使用这个vscode插件来进行代码格式化,这里提供一份目前个人使用的配置:

.prettierrc

{
    "arrowParens": "always",
    "bracketSameLine": false,
    "bracketSpacing": true,
    "embeddedLanguageFormatting": "auto",
    "htmlWhitespaceSensitivity": "css",
    "insertPragma": false,
    "jsxSingleQuote": false,
    "printWidth": 120,
    "proseWrap": "preserve",
    "quoteProps": "as-needed",
    "requirePragma": false,
    "semi": true,
    "singleAttributePerLine": false,
    "singleQuote": false,
    "tabWidth": 2,
    "trailingComma": "none",
    "useTabs": true,
    "vueIndentScriptAndStyle": false,
    "endOfLine": "lf"
}
本博客发布了一篇关于Prettier 格式化配置,建议使用那份,博客内搜索Prettier 即可。

eslint配合Prettier做代码检查

eslint恶心人,但是它又能很好的规范你的代码,又爱又恨。

安装依赖:

pnpm i @vue/eslint-config-typescript eslint eslint-config-prettier eslint-plugin-prettier eslint-plugin-vue vite-plugin-eslint -D

vite.config.ts加入配置:

import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import EslintPlugin from "vite-plugin-eslint";

// https://vitejs.dev/config/
export default ({ mode }) => {
    process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };

    //插件
    const plugins = [
        vue(),
        EslintPlugin({
            cache: false,
            fix: true
        })
    ];
    
    return defineConfig({
        plugins,
    });
};

在项目根目录,与src同级创建eslint的配置文件:

.eslintrc.js

module.exports = {
    root: true,
    env: {
        node: true
    },
    extends: [
        "plugin:vue/vue3-essential",
        "eslint:recommended",
        "@vue/eslint-config-typescript",
        "plugin:prettier/recommended"
    ],
    rules: {
        "vue/multi-word-component-names": "off",
        "no-unused-vars": ["error", { vars: "all", args: "none", ignoreRestSiblings: false }],
        "vue/no-unused-components": "off" // 取消检查未使用的组件
    }
};

.eslintignore

这个文件暂时空白,主要是用于添加文件黑名单,这样这些对应的文件不会走eslint检查。

小技巧:

当你有一个函数,它接收3个参数,而你只需要用到最后一个参数,这个时候eslint其实会报错的,我们可以通过给不用的参数名前缀加下划线来实现忽略,vue-router的例子:

import { createRouter, createWebHashHistory } from "vue-router";
import routes from "./routes";

const router = createRouter({
    history: createWebHashHistory(),
    routes,
    scrollBehavior(_to, _from, savedPosition) {
        return savedPosition || { top: 0 };
    }
});

export default router;

添加一个vscode的项目配置文件:

.vscode/settings.json

{
    "eslint.validate": ["javascript", "javascriptreact", "vue", "typescript", "typescriptreact"],
}

至此完成。

路径别名

vite.config.ts

import { defineConfig, loadEnv } from "vite";
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";

// https://vitejs.dev/config/
export default ({ mode }) => {
    process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };

    //插件
    const plugins = [
        vue(),
    ];

    return defineConfig({
        plugins,
        resolve: {
            alias: {
                "@": fileURLToPath(new URL("./src", import.meta.url))
            },
            extensions: [".js", ".json", ".jsx", ".mjs", ".ts", ".tsx", ".vue"]
        },
    });
};

ts.config.json

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        },
    },
}

推荐在.vscode/settings.json中加入:

"typescript.suggest.paths": true,
"javascript.suggest.paths": true

明明我安装了Path Intellisense和 Path Autocomplete,但是不配这两个总是不出来路径提示,唉!

img图片路径相关

vite的静态资源引入相对于vue cli还是差了很多的,对于动态绑定图片路径基本没办法,官方提供的解决方案也是有各种限制,复用度基本为0。

首先,只有在vue组件中,你的img图片src属性可以直接绑定显式路径。

<img src="@/assets/images/xxx.png" />

除了可以使用路径别名,你还可以使用相对路径。

<img src="../assets/images/xxx.png" />

但是便利性也仅限于此了,如果你想中间动态拼接一个关键词,哦吼!不好意思,使用路径别名@/将无法正确解析。

然后官方提供了几种解决方案:

import引入

你可以直接import引入图片资源然后return出去。

<script lang="ts">
import { defineComponent } from "vue";
import testImg from "@/assets/images/xxx.jpg";

export default defineComponent({
    setup() {
        return {
          testImg
        };
    }
});
</script>

使用的时候绑定即可。

<img :src="testImg" />

但是这种方式未免有点麻烦,如果图片地址是从配置文件里面拿的,就不好弄了。

new URL引入

如果存在动态变量的情况下,官方又提供了new URL拼接的方式。

官方文档: new URL

function getImageUrl(name: string) {
  return new URL(`../assets/images/${name}`, import.meta.url).href
}
<img :src="getImageUrl('xxx.jpg')" />

最新vite4测试中发现,new Url确实可以作为require函数的替代品,此时../assets是相对于这个函数所在的文件的,为此我提供一个自己封装的hooks代码。

useAssets.ts

export default function useAssets() {
    /** 前缀补路径斜杠 */
    function addPrefixSlash(path: string) {
        return path.startsWith("/") ? path : `/${path}`;
    }
    /** 前缀去路径斜杠 */
    function removePrefixSlash(path: string) {
        return path.startsWith("/") ? path.slice(1) : path;
    }

    /**
     * @description: 获取assets路径
     * @param {string} path
     * @Date: 2023-08-03 17:35:35
     * @Author: copilot
     */
    function getAssetsUrl(path: string) {
        return new URL(`../assets/${removePrefixSlash(path)}`, import.meta.url).href;
    }

    /**
     * @description: 获取images路径
     * @param {string} path
     * @Date: 2023-08-03 17:41:23
     * @Author: copilot
     */
    function getImagesUrl(path: string) {
        return new URL(`../assets/images/${removePrefixSlash(path)}`, import.meta.url).href;
    }

    return {
        getAssetsUrl,
        getImagesUrl
    };
}

事实上其实也不必非得封装成一个hooks,个人理解这个功能是用于替代webpack中的require函数的,它可以说不是一JavaScript环境应该提供的功能,所以我没有把他封装成普通工具函数丢到utils工具函数中去。

import.meta.glob

上面两种方案都有各自限制,我看了一些文档,官方提供了这么一个方法用于以字符串形式导入资源。

其原理和node的glob差不多,我们先通过正则匹配到一个目录下的所有文件,然后得到一个对象,对象的key就是文件的path,值则是一个方法,方法是一个promise,我们需要在then中得到一个res对象,通过res.default得到正确资源路径。

但是由于是一个异步的内容,我们不得不自己在组件中创建一个ref变量做占位,然后等待promise完成后将ref的值改变,图片src则绑定这个ref值。

但是这也很痛苦,我需要书写特别多的代码来实现这个功能。

const getImgSrc = (path: string) => {
    const path = `/src/assets/images/${name}`;
    const modules = import.meta.globEager('/src/assets/images/*');
    const fn = modules[`/src/assets/images/${path}`];
    if(typeof fn === "function") {
      return fn();
    } else {
      return Promise.reject(new Error("资源不存在"));
    }
}

export default getImgSrc
<script lang="ts">
import { defineComponent, ref } from "vue";
import getImgSrc from '@/utils/imgUtil';

export default defineComponent({
    setup() {
        const imgSrc = ref("");
        getImgSrc("xxx.jpg").then(res=>{
          imgSrc.value = res.default;
        }).catch(()=>{});
         
        return {};
    },
});
</script>

相当痛苦。

我感觉目前vite对于静态资源的导入,真的没有什么好办法了,我们知道,引入assets中的图片资源是有优化处理的,比如hash后缀防止缓存,base64处理,使用图片压缩插件还能压缩图片,但是这些好的效果在稍微复杂一点的需求的情况下,基于上面三种方案,就不得不去做妥协,可能是我已经习惯了webpack那种方式带来的便捷了。

最终我的方案 public

由于我的需求是维护一份大的配置文件,这个文件导出对象,对象里会有对应的图片资源路径,这样方便共用同一个项目结构的情况下,切换不同的内容。

我通过更改环境变量中的key来指定使用对象中哪个属性。

基于这个场景,我发现我要么在每个组件里写上一个相同的imgPath方法,否则没有更便捷的方式了,想来想去我选择了最省事的办法,就是使用public,这样我不需要书写动态引入的方法,所有的资源文件全部绝对路径引入。

为此我不得不放弃assets带来的资源优化特性。

具体代码就不写了,没啥特殊的。

分类: vue 项目实战 标签: 自动引入环境变量eslint路径别名vue3TypeScripttsvite4全局方法vw的转换vwvarletPrettierimg图片路径静态资源引入

评论

暂无评论数据

暂无评论数据

目录