vue3+ts+vite4的一些踩坑实录
全局方法挂载和类型声明
全局挂载还是很简单的,照着官方的例子写就可以了,我的做法是创建了一个单独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转换,他有一个优势:
- 不需要单独的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.ts
和components.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带来的资源优化特性。
具体代码就不写了,没啥特殊的。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
暂无评论数据