前言
在上篇 工欲善其事必先利其器(前端代码规范) 中,我们了解代码规范的基本配置,本篇以 Vue3 + typescript
为例结合 vue-router + pinia + ant-design-vue + vue-i18n + vite
配置一个简单的项目模板
准备
初始化一个项目,目录结构大体如下
目录结构
├─.vscode
| ├─settings.json
├─.eslintignore
├─.eslintrc
├─.prettierignore
├─.prettierrc
├─.stylelintignore
├─.stylelintrc
├─index.html
├─package.json
├─tsconfig.json
├─vite.config.dev.ts
├─vite.config.prod.ts
├─types
├─src
| ├─App.vue
| ├─main.ts
| ├─utils
| ├─store
| ├─router
| ├─pages
| ├─i18n
| ├─components
| ├─assets
| ├─api
初始化
需要初始化的模块在 main.ts
中可以看到大概
main.ts
import VueDOMPurifyHTML from 'vue-dompurify-html'
import components from '@/components'
import directive from '@/directive'
import { createApp } from 'vue'
import router from '@/router'
import App from './App.vue'
import pinia from '@/store'
import i18n from '@/i18n'
// 全局样式
import '@/assets/style/index.less'
/**
* bootstrap
*/
async function bootstrap(): Promise<void> {
const app = createApp(App)
app.use(pinia).use(i18n).use(components).use(router).use(VueDOMPurifyHTML).use(directive)
app.mount('#app', true)
}
bootstrap()
通过 main.ts
可以看到核心有以下模块需要处理
- components,需要全局注册的组件
- directive,自定义全局指令
- router,路由
- pinia,store
- i18n,国际化
vue-dompurify-html 是安全的 v-html
指令库
模块处理
为了方便管理和维护,最好约定同类模块的内容都统一收拢到对应模块内统一管理
组件
组价统一放到 src/components
管理,非业务型通用组件可以放到 src/components/common
下,然后在 src/components/index.ts 中统一处理组件的全局注册,如
import type { App } from 'vue'
import SwitchLocale from './common/SwitchLocale.vue'
export default {
install(app: App): void {
app.component('SwitchLocale', SwitchLocale)
}
}
自定义指令
自定义指令
收拢在 src/directive
文件夹内,在 src/directive/index.ts 中统一处理指令的全局注册,如
import type { App } from 'vue'
import resize from './resize'
import wheel from './whell'
export default {
install(app: App): void {
app.directive('resize', resize)
app.directive('wheel', wheel)
}
}
国际化
国际化
相关内容收拢在 src/i18n
文件夹内
- 初始化 vue-i18n
import { createI18n } from 'vue-i18n'
import messages from './langs'
export const fallbackLocale = 'zh-CN'
const i18n = createI18n({
legacy: false, // 使用Composition API,这里必须设置为false
globalInjection: true,
global: true,
locale: fallbackLocale,
fallbackLocale, // 默认语言
messages
})
// 方便在 Composition API 及 Hooks 写法下使用,如 import { $t } from '@/i18n'
export const $t = i18n.global.t
- 处理切换语言下拉选项列表
import messages from './langs'
export type LocaleType = keyof typeof messages
const lacaleKeys = Object.keys(messages)
export const lacaleList = lacaleKeys.map((key: LocaleType) => {
return { name: messages[key].localeName, key }
})
// 暴露 LacaleOption 类型,因为懒得写声明,直接用 PickArrayItem 推导
export type LacaleOption = PickArrayItem<typeof lacaleList>
// types/index.d.ts
declare type PickArrayItem<T> = T extends Array<infer G> ? G : never
- 封装并导出切换语言接口
import { locale } from 'dayjs'
/**
* 切换语言
*
* @param {string} newlocale locale
* @returns {void}
*/
export function changeLocale(newlocale = fallbackLocale): void {
i18n.global.locale.value = newlocale
// 为了这里能正常运作,langs里的命名要按照dayjs的命名来
locale(newlocale.toLocaleLowerCase())
}
- 语言
provider
注入处理
// src/i18n/langs/zh-CN.ts
import zhCN from 'ant-design-vue/es/locale/zh_CN'
// 把 ant-design-vue 的引入
export default {
...zhCN,
localeName: '简体中文', // 切换语言下拉列表展示的语言名称
}
// src/i18n/index.ts
export type LangsLocale = (typeof messages)['zh-CN']
/**
* 获取 ant-design-vue 多语言
*
* @returns {LangsLocale} 语言配置
*/
export function getLocale(localeName: LocaleType): LangsLocale {
return messages[localeName]
}
// src/App.vue
<template>
<a-config-provider :locale="getLocale(i18nStore.locale.key)">
<router-view />
</a-config-provider>
</template>
<script lang="ts" setup>
import { getLocale } from '@/i18n'
import { useI18nStore } from '@/store'
const i18nStore = useI18nStore()
</script>
完整内容请查看 src/i18n/index.ts
路由
路由配置,这个参照 vue-router 配置即可
import { $t } from '@/i18n'
import { apiTokenSession } from '@/utils'
import { notification } from 'ant-design-vue'
import { createRouter, createWebHistory } from 'vue-router'
// 路由配置
import { routes } from './routeRecordRaw'
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach((to, _from, next) => {
if (['/login'].includes(to.path)) {
next()
} else {
// 判断token是否失效
if (apiTokenSession()) {
next()
} else {
notification.info({
message: $t('pleaseLogin'),
duration: 2
})
next({ path: '/login' })
}
}
})
export default router
pinia
store
相关内容收拢在 src/store
文件夹内,在 src/store/modules 文件夹拆分各个模块并提供模块 use
,在src/store/index.ts 中统一暴露出去,这里推荐使用 Hooks
写法,pinia 文档 也是这种写法
// src/store/modules/i18n.ts
import type { LacaleOption } from '@/i18n'
import { defineStore } from 'pinia'
import { i18nLocal } from '@/utils'
import { changeLocale } from '@/i18n'
export const useI18nStore = defineStore('i18n', {
state: () => {
const store = i18nLocal()
changeLocale(store.key)
return { locale: store }
},
actions: {
changeLocale(locale: LacaleOption) {
i18nLocal(locale)
changeLocale(locale.key)
this.locale = locale
}
}
})
// src/store/index.ts
import { createPinia } from 'pinia'
export * from './modules/user'
export * from './modules/i18n'
const pinia = createPinia()
export default pinia
Storage
在上面的配置中,你肯定遇到了一些需要存储的数据,比如当前选择语言的i18n 配置信息
、api token
等等,因为也是常用的模块,收拢起来比较好管理
Storage
统一放在 src/utils/storage
文件夹内管理, 以 sessionStorage
为例
在 src/utils/storage/session 中统一管理 sessionStorage
先在 src/utils/storage/session/base.ts 封装好基本的 sessionStorage
模块
export type SessionDB = string | null | void
export function session(name: string): string | null
export function session(name: string, value: string): void
/**
* sessionStorage
*
* @param {string} key 数据标识
* @param {string} value 数据
* @returns {SessionDB} SessionDB
*/
export function session(key: string, value?: string): SessionDB {
if (value !== undefined) {
return sessionStorage.setItem(key, value)
}
return sessionStorage.getItem(key)
}
/**
* 清空sessionStorage
*/
export function clearAllSession(): void {
sessionStorage.clear()
}
/**
* 根据key值,删除对应的sessionStorage
*
* @param {string} key Session key
*/
export function removeSession(key: string): void {
sessionStorage.removeItem(key)
}
然后再在 src/utils/storage/session/modules 处理各个 sessionStorage
模块,以 apiToken
为例
import type { TokenResponse } from '@/api'
import type { SessionDB } from '../base'
import { session, removeSession } from '../base'
// -------------api token-------------
const _API_TOKEN_ = '_API_TOKEN_'
export interface TokenSession extends TokenResponse {
startTime: number
}
/**
* 清空apiToken缓存
*
* @returns {SessionDB} SessionDB
*/
export function removeApiTokenSession(): SessionDB {
return removeSession(_API_TOKEN_)
}
/**
* 用户token
*
* @param {string} value value
* @returns {SessionDB} SessionDB
*/
export function apiTokenSession(value?: TokenResponse): SessionDB {
if (value !== undefined) {
return session(_API_TOKEN_, JSON.stringify({ ...value, startTime: Date.now() }))
}
const db = session(_API_TOKEN_)
if (db) {
const token = JSON.parse(db) as TokenSession
const { startTime, expires_in } = token
if (Date.now() - startTime > expires_in) {
removeApiTokenSession()
return
}
return token.access_token
}
return
}
这里封装了 token
数据的存取以及过期处理,如果考虑 JSON.parse
报错也可以在这里统一加 try catch
处理,这样外部使用就无需关心这些细枝末节的问题,而且 typescript
类型也可以统一处理,外部引入即可
api
api
模块统一放在 src/api
下管理
首先在 src/api/request.ts
基于axios 二次封装基本的 request
,这里就不过多介绍了,详细请参考 axios 文档
其次是各个模块的 api
,以登录为例,主要是做好类型声明
// src/api/login/index.ts
import type { AxiosPromise } from 'axios'
import request from '../request'
const prefix = '/login'
export interface TokenResponse {
access_token: string
expires_in: number
}
export interface LoginFormData {
username: ''
password: ''
remember: true
}
/**
* 登录
*
* @param {FormData} data 表单信息
* @returns {AxiosPromise} TokenData
*/
export function login(data: FormData): AxiosPromise<ServerResponse<TokenResponse>> {
return request({
url: `${prefix}/oauth/token`,
method: 'POST',
data,
headers: {
Authorization: 'Basic d2ViQXB=='
}
})
}
/**
* 注销
*
* @returns {AxiosPromise} logout res
*/
export function logout(): AxiosPromise<ServerResponse<null>> {
return request({ url: `${prefix}/logout`, method: 'GET' })
}
其中 ServerResponse
是后端封装的一层数据,在 api.d.ts
declare interface PageResponse<T> {
total: number
rows: T[]
code?: number
msg?: string
pages?: number
currentPage: number
size: number
}
declare interface ServerResponse<T> {
code: number
msg: string
data: T
}
至此,基本的模块就处理完成了
配置
处理完各个模块,接下来就是工程配置了,在上篇已经给出规范相关的配置,这里就不再详细说明了
tsconfig-json
tsconfig-json
详细配置说明可以参看tsconfig-json、完整compilerOptions选项
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"strictFunctionTypes": false,
"jsx": "preserve",
"baseUrl": ".",
"allowJs": true,
"sourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"experimentalDecorators": true,
"lib": ["dom", "esnext"],
"types": ["vite/client"],
"typeRoots": ["./node_modules/@types/", "./types"],
"noImplicitAny": false,
"skipLibCheck": true,
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "types/*.d.ts"],
"exclude": ["node_modules", "dist", "**/*.js"]
}
因为全局的.d.ts
收集在 types
下需要配置 typeRoots
,默认的 ./node_modules/@types/
也需要包含上来
vite Client 类型支持参考 Client Types 配置
vite
vite.config.dev.ts
文件
import path from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import autoImport from 'unplugin-auto-import/vite'
export default defineConfig({
base: './',
plugins: [
vue(),
autoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts'
})
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
host: '0.0.0.0',
port: 8000,
open: true,
https: false,
proxy: {
'/dev': {
target: 'http://xxx-dev.com',
changeOrigin: true,
rewrite: p => p.replace(/^\/dev/, '')
},
}
}
})
package.json
增加启动命令
"scripts": {
"dev": "vite -c ./vite.config.dev.ts --mode development"
}
可以使用@vitejs/plugin-vue-jsx 插件增加对 jsx
的支持
针对浏览器兼容性的可参考 vite 构建生产版本
如果没有其他特殊要求,打包也可以使用该配置,只需配置打包命令即可
"build": "vite build -c ./vite.config.dev.ts --mode production",
至此,一个项目的基本配置就完成了,接下来看看还有什么是可以优化和完善的
优化
优化永远是进行时,根据不同情况需要做相应的取舍,有兴趣可以看看我的这篇简单分析聊聊前端性能优化
启动优化
通过 optimizeDeps.include
添加需要预编译的包
optimizeDeps: {
include: ['vue', 'vue-i18n', 'vue-router']
}
也可以使用vite-plugin-optimize-persist 和vite-plugin-package-config 自动预构建
除了预编译,最好的方案还是减少不必要的编译,需要配合 esbuild.exclude
配置,因为和下文打包有类似,这里不详细介绍了
打包体积优化
vite.config.prod.ts
文件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import autoImport from 'unplugin-auto-import/vite'
export default defineConfig({
base: '/',
define: {
'process.env': process.env
},
plugins: [
autoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts'
}),
vue(),
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
css: {
preprocessorOptions: {
less: {
javascriptEnabled: true
}
}
},
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
package.json
增加打包命令
"scripts": {
"build": "vite build -c ./vite.config.prod.ts --mode production"
}
优化之前先通过rollup-plugin-visualizer来分析打包结果得到优化方向
安装 rollup-plugin-visualizer
,vite.config.prod.ts
改造如下
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import autoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
const lifecycle = process.env.npm_lifecycle_event
const plugins = [
autoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts'
}),
vue(),
]
if (lifecycle === 'report') {
plugins.push(visualizer({ open: true, brotliSize: true, filename: 'visualizer/report.html' }))
}
export default defineConfig({
base: '/',
define: {
'process.env': process.env
},
plugins,
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
package.json
增加打包分析预览命令并执行
"scripts": {
"report": "vite build -c ./vite.config.prod.ts --mode production"
}
由于 ant-design-vue
是全局引入,所以即使未使用也打包进来了
按需引入
利用 rollup
的 Tree shaking
能力可以很容易做到,只要把全局引用改为单个引用即可
手动处理按需引入也可以,不过还是使用自动按需引入插件unplugin-vue-components比较香
安装 unplugin-vue-components
,vite.config.prod.ts
改造如下
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import autoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
const lifecycle = process.env.npm_lifecycle_event
const plugins = [
autoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts',
resolvers: [AntDesignVueResolver()]
}),
Components({
dts: 'types/components.d.ts',
// dirs 指定组件所在位置,默认为 src/components
// 可以让我们使用自己定义组件的时候免去 import 的麻烦
// dirs: ['src/components/'],
// 配置需要将哪些后缀类型的文件进行自动按需引入
extensions: ['vue', 'md'],
resolvers: [AntDesignVueResolver()]
}),
vue()
]
if (lifecycle === 'report') {
plugins.push(visualizer({ open: true, brotliSize: true, filename: 'visualizer/report.html' }))
}
export default defineConfig({
base: '/',
define: {
'process.env': process.env
},
plugins,
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
然后去掉 main.ts
中的全局注册
import { createApp } from 'vue'
import I18n from '@/i18n'
import App from './App.vue'
import Router from '@/router'
createApp(App).use(Router).use(I18n).mount('#app', true)
App.vue
使用a-layout
组件
<template>
<a-layout>
<router-view />
</a-layout>
</template>
结果如下,体积减少了很多
如果有需要还可以使用 vite-plugin-compression 进行压缩
分包
此时除了路由的懒加载自动分包其他包是打到一起的,分包需更改 rollupOptions
的配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import autoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
const lifecycle = process.env.npm_lifecycle_event
const plugins = [
autoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts',
resolvers: [AntDesignVueResolver()]
}),
Components({
resolvers: [AntDesignVueResolver()]
}),
vue()
]
if (lifecycle === 'report') {
plugins.push(visualizer({ open: true, brotliSize: true, filename: 'visualizer/report.html' }))
}
export default defineConfig({
base: '/',
define: {
'process.env': process.env
},
plugins,
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/.pnpm/')[1].split('/')[0].toString()
}
},
// 资源也做分类
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
}
})
打包加速 & 网络加速
打包加速实际上有两个大方向
- 减少不必要编译
- 多线程编译
这里我们仅关注 减少不必要编译,这就需要将静态资源提取出来,同时为了能加快网络加载资源的速度,对于这些不会经常改动的资源可以使用 CDN 资源
vite
相关的 CDN 库可以考虑 vite-plugin-cdn-import和 vite-plugin-cdn-import-async
回到编译本身,使用了库肯定还是有一定的"编译"量,这里选择手动配置,以 unpkg 为例
将 index.html
改造如下,类似ant-design-vue
这种库可以根据实际情况判断是需要按需打包还是可以使用 CDN 全引,由于 pinia
使用到 vue-demi
所以需要先引入 vue-demi
,像three
这些资源只有进入某些场景才使用到的可以异步加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://unpkg.com/ant-design-vue@3.2.15/dist/antd.min.css" rel="stylesheet" />
<title>Demo</title>
<script src="https://unpkg.com/vue@3.2.47/dist/vue.runtime.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi@0.13.11"></script>
<script src="https://unpkg.com/pinia@2.0.30"></script>
<script src="https://unpkg.com/axios@1.3.2/dist/axios.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/dayjs.min.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/plugin/customParseFormat.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/plugin/weekday.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/plugin/localeData.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/plugin/weekOfYear.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/plugin/weekYear.js"></script>
<script src="https://unpkg.com/dayjs@1.11.7/plugin/advancedFormat.js"></script>
<script src="https://unpkg.com/ant-design-vue@3.2.15/dist/antd.min.js"></script>
<script src="https://unpkg.com/vue-i18n@9.2.2/dist/vue-i18n.runtime.global.prod.js"></script>
<script src="https://unpkg.com/vue-router@4.1.6/dist/vue-router.global.prod.js"></script>
<script async src="https://unpkg.com/three@0.149.0/build/three.min.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
配置完 html
还需要告诉 rollup
这些资源不需要打包,rollupOptions
,需要配置external
rollupOptions: {
external: ['three', 'dayjs', 'ant-design-vue', 'vue', 'axios', 'vue-i18n', 'vue-router', 'vue-demi', 'pinia']
}
但是光屏蔽打包还不行,还需要告知 rollup
这些被屏蔽的资源改从哪里引,使用 rollup-plugin-external-globals 可以很方便处理
rollupOptions: {
plugins: [
externalGlobals({
vue: 'Vue',
three: 'THREE',
'ant-design-vue': 'antd',
axios: 'axios',
dayjs: 'dayjs',
'vue-router': 'VueRouter',
'vue-i18n': 'VueI18n',
'vue-demi': 'VueDemi'
})
],
external: ['three', 'dayjs', 'ant-design-vue', 'vue', 'axios', 'vue-i18n', 'vue-router', 'vue-demi', 'pinia']
}
这个时候 externalGlobals
和 autoImport
执行顺序冲突了,具体可以查看这个issues,vite.config.prod.ts
改造如下
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import autoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
const lifecycle = process.env.npm_lifecycle_event
const plugins = [
autoImport({
imports: ['vue', 'vue-router'],
dts: 'types/auto-imports.d.ts',
resolvers: [AntDesignVueResolver()]
}),
Components({
resolvers: [AntDesignVueResolver()]
}),
vue(),
{
...externalGlobals({
vue: 'Vue',
three: 'THREE',
'ant-design-vue': 'antd',
// swiper: 'Swiper',
axios: 'axios',
dayjs: 'dayjs',
'hls.js': 'Hls',
'vue-router': 'VueRouter',
'vue-i18n': 'VueI18n',
'vue-demi': 'VueDemi'
}),
enforce: 'post',
apply: 'build'
}
]
if (lifecycle === 'report') {
plugins.push(visualizer({ open: true, brotliSize: true, filename: 'visualizer/report.html' }))
}
export default defineConfig({
base: '/',
define: {
'process.env': process.env
},
plugins,
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/.pnpm/')[1].split('/')[0].toString()
}
},
// 资源也做分类
chunkFileNames: 'static/js/[name]-[hash].js',
entryFileNames: 'static/js/[name]-[hash].js',
assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
}
}
}
})
细节处理
还有一些细节可以优化
.vue
文件在 template
使用 $t()
没提示处理
因为此时$t
的类型声明是 vue-i18n.d.ts
提供的,不是实例化的,其类型是 string
,所以没有具体提示
一种解决方案是换一个字段,在 types/global.d.ts
中写入如下内容
import { i18n } from '@/i18n'
declare module 'vue' {
export interface ComponentCustomProperties {
$tt: typeof i18n.global.t
}
}
i18n
模块内容改造如下
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import modules from './langs'
export const i18n = createI18n({
legacy: false, // 使用Composition API,这里必须设置为false
globalInjection: true,
global: true,
locale: 'zh',
fallbackLocale: 'zh',
messages: modules
})
export default {
install: (app: App) => {
app.config.globalProperties.$tt = i18n.global.t
}
}
此时在vue
文件中使用 $tt
可以有提示
不过这种方式不太好看就是了,个人认为是添加一个 $t
的重载才是比较好的方式,不过重载未生效,但是写类型推导的过程还是挺有趣的
export type LangType = (typeof messages)['zh-CN']
export type ConnectKey<Prefix, K> = K extends string ? `${Prefix & string}.${K}` : `${Prefix & string}`
export type GetLangKey<T, K> = ConnectKey<K, T>
export type DeepGetLangKeys<T> = {
[K in keyof T]: GetLangKey<DeepGetLangKeys<T[K]>, K>
}[keyof T]
export type LangKey = DeepGetLangKeys<LangType>
function test(k: LangKey): string {
return ''
}
// 这里 test 入参有提示
test('')
treer
README.md
的完整目录说明,可以使用 treer 生成目录结构
# 全局安装
npm i -g treer
# 生成目录
treer -d 项目名 -e t.md -i "/node_modules|.git|.husky/"
.eslintrc
需要补充 vue/setup-compiler-macros
配置,用以解决.vue
文件运行时j解析问题
"env": {
"browser": true,
"es2021": true,
"vue/setup-compiler-macros": true
}
注意
- 按照此时配置,打包后有一个
i8n
问题,如果翻译文件配置了这种动态的形式:total: '共 {total} 条'
,使用方式:$t('total', 10)
。构建后{total}
没有动态替换,需要移除vue-i18n
的 CND 配置,或者不使用runtime
版本,即把vue-i18n.runtime.global.prod.js
改为vue-i18n.global.prod.js
最后
本文完整配置可以查阅vue3-template(请注意时效性)
到这里一个简单能用的项目模板就搭建完成了,至于测试、监控、 CI/CD 等等,篇幅原因这里就不详细介绍了
附录
- Vue3
- vue-router
- vue-i18n
- pinia
- vite
- ant-design-vue
- ant-design-vue 国际化
- axios 完档
- 字节跳动静态资源库
- BootCDN 稳定、快速、免费的前端开源项目 CDN 加速服务,共收录了 3934 个前端开源项目
- unpkg 公共 cdn