「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」
上次发表了一篇写了8个晚上2-3小时,用vite重构了vuecli的模版项目 - 掘金 (juejin.cn),当时初始化项目使用的是createVue脚手架创建的基础项目结构,如果我们不使用它,如何搭建基础项目呢,让我们一起来看看吧。
本次项目搭建使用的是pnpm包管理器,nodejs版本v17.2.0
初始化vite项目
- 执行脚本命令
pnpm create vite
- 创建项目名称
- 选择创建vue项目
- 选择vue-ts项目
✔ Project name: … vue-project
? Select a framework: › - Use arrow-keys. Return to submit.
vanilla
❯ vue
react
preact
lit
svelte
? Select a variant: › - Use arrow-keys. Return to submit.
vue
❯ vue-ts
- 一个基础的基于vite+vue+ts的基础项目就完成了,下面我们开始配置我们的项目模版
目录
- √ 配置 ip 访问项目
- √ 配置多环境变量
- √ 集成 Tsx
- √ 配置 alias 别名
- √ Sass 全局样式
- √ 识别 nodejs 内置模块
- √ 静态资源使用
- √ Vue-router
- √ Pinia 状态管理
- √ Eslint + Prettier 统一开发规范
- √ husky + lint-staged 提交校验
- √ 使用 Mock 数据
✅ 配置 ip 访问项目
- vite 启动后出现 “ Network: use --host to expose ”
vite v2.3.7 dev server running at:
> Local: http://localhost:3000/
> Network: use `--host` to expose
- 是因为 IP 没有做配置,所以不能从 IP 启动,需要在 vite.config.js 做相应配置: 在 vite.config.js 中添加 server.host 为 0.0.0.0
export default defineConfig({
plugins: [vue()],
// 在文件中添加以下内容
server: {
host: '0.0.0.0'
}
})
- 重新启动后显示
vite v2.3.7 dev server running at:
> Local: http://localhost:3000/
> Network: http://192.168.199.127:3000/
✅ 配置多环境变量
- 在生产环境,会把 import.meta.env 的值转换成对应真正的值
- 添加环境变量文件,每个文件写入配置,定义 env 环境变量前面必须加 VITE_
.env.development
# must start with VITE_
VITE_ENV = 'development'
VITE_OUTPUT_DIR = 'dev'
.env.production
# must start with VITE_
VITE_ENV = 'production'
VITE_OUTPUT_DIR = 'dist'
.env.test
# must start with VITE_
VITE_ENV = 'test'
VITE_OUTPUT_DIR = 'test'
- 修改 scripts 命令
--mode用来识别我们的环境
"dev": "vite --mode development",
"test": "vite --mode test",
"prod": "vite --mode production",
- 在项目中访问
console.log(import.meta.env)
- typescript 智能提示
- 修改
src/env.d.ts文件,如果没有创建一个
/// <reference types="vite/client" />
interface ImportMetaEnv extends Readonly<Record<string, string>> {
readonly VITE_ENV: string; // 环境
readonly VITE_OUTPUT_DIR: string; // 打包目录
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
✅ 集成 Tsx
- 安装依赖
pnpm i -D @vitejs/plugin-vue-jsx
- 修改 vite.config.ts 配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx({
include: /\.(jsx|tsx)/
})
],
server: {
host: '0.0.0.0'
}
})
✅ 配置 alias 别名
- 文档:cn.vitejs.dev/config/#res…
- 修改 vite.config.ts 配置
resolve: {
alias: {
"@": "/src",
},
},
✅ Sass 全局样式
- 安装依赖
使用
dart-sass, 安装速度比较快,大概率不会出现安装不成功
pnpm i -D sass
- 使用
每个页面自己对应的样式都写在自己的 .vue 文件之中
scoped它顾名思义给 css 加了一个域的概念。
<style lang="scss">
/* global styles */
</style>
<style lang="scss" scoped>
/* local styles */
</style>
css modules
- 目前测试只有在 tsx 中可以正常使用,vue-template 中可以导入在 js 中使用,
<template>中还不知道怎么使用 - 定义一个
*.module.scss或者*.module.css文件 - 在 tsx 中使用
import { defineComponent } from 'vue'
import classes from '@/styles/test.module.scss'
export default defineComponent({
setup() {
console.log(classes)
return () => {
return <div class={`root ${classes.moduleClass}`}>测试css-modules</div>
}
}
})
vite 识别 sass 全局变量
- vite.config.js 添加配置
css: {
preprocessorOptions: {
scss: {
additionalData: `
@import "@/styles/mixin.scss";
@import "@/styles/variables.scss";
`,
},
},
},
✅ 识别 nodejs 内置模块
- path 模块是 node.js 内置的功能,但是 node.js 本身并不支持 typescript,所以直接在 typescript 项目里使用是不行的
- 解决方法:安装@types/node
pnpn i -D @types/node
- 在 vite.config.js 中使用
import { resolve } from 'path'
✅ 静态资源使用
// staticTest.vue
import img from '@/assets/images/图片.jpg' // 返回图片资源路径
import demo from './demo.tsx?url' // 显式加载资源为一个 URL
import test from '@/test/test?raw' // 以字符串形式加载资源
import Worker from '@/test/worker?worker' // 如果是计算量很大的代码,可以使用 worker ,开启新的线程加载,与主线程通信
import jsonText from '@/test/jsonText.json' // 读取 json 文件
console.log('静态图片--', img)
console.log('显式加载资源的url--', demo)
console.log('以字符串形式加载资源--', `类型${typeof test}`, test)
console.log('读取json--', jsonText)
const worker = new Worker()
worker.onmessage = function (e) {
console.log('worker监听---', e)
}
动态引入图片
-
使用
new URL和import.meta.url时的问题import.meta.url获取到的是当前页面完整 url 地址new URL中必须填写相对路径- 打包不支持中文路径,暂时没解决
[vite:asset-import-meta-url] ENOENT: no such file or directory, open '/Users/zhangyong/code/oneself/template/vite-vue3-h5-template/src/assets/images/png/\u5E74\u7EC8\u603B\u7ED3.png'
// src/components/HelloWorld.vue
new URL('../assets/images/png/year.png', import.meta.url).href
// import.meta.url 获取到的地址:http://192.168.124.4:3000/src/components/HelloWorld.vue?t=1641037446646
// 拼接后的地址:http://192.168.124.4:3000/src/assets/images/png/%E5%B9%B4%E7%BB%88%E6%80%BB%E7%BB%93.png
✅ Vue-router4
- 文档:next.router.vuejs.org/zh/installa…
- composition-api 使用:next.router.vuejs.org/zh/guide/ad…
1. 安装依赖
pnpm install vue-router@4
2. 配置路由 api
- 在 src 目录下,新建 router 文件夹,并在文件夹内创建
- index.ts 管理路由 api
- router.config.ts 管理路由信息
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from './router.config'
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// router/router.config.ts
import { RouteRecordRaw, createRouter, createWebHistory } from 'vue-router'
export const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
redirect: '/home',
meta: {
title: '首页',
keepAlive: false
},
component: import('@/views/layouts/index.vue'),
children: [
{
path: '/home',
name: 'Home',
component: import('@/views/home.vue'),
meta: { title: '首页', keepAlive: false, showTab: true }
}
]
}
]
3. mian 中引入 router
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
// 引入全局样式
import '@/styles/index.scss'
const app = createApp(App)
app.use(router)
app.mount('#app')
4. app.vue 和 layout 配置 router-view
// app.vue
<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
console.log('查看全局环境',import.meta.env);
</script>
<template>
<router-view />
</template>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
// layouts/index.vue
<script setup lang='ts'>
import { useRoute } from 'vue-router';
const route = useRoute()
console.log(route.meta);
</script>
<template>
<div class="layout-content">
<keep-alive v-if="route.meta.keepAlive">
<router-view></router-view>
</keep-alive>
<router-view v-else></router-view>
</div>
</template>
<style lang='scss' scoped>
</style>
✅ Pinia 状态管理
- 文档:pinia.vuejs.org/
- 参考资料:juejin.cn/post/704919…
- Pinia 的特点:
- 完整的 typescript 的支持;
- 足够轻量,压缩后的体积只有 1.6kb;
- 去除 mutations,只有 state,getters,actions(这是我最喜欢的一个特点);
- actions 支持同步和异步;
- 没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
- 无需手动添加 store,store 一旦创建便会自动添加;
安装依赖
pnpm i pinia
创建 Store
- 新建 src/store 目录并在其下面创建 index.ts,导出 store
// src/store/index.ts
import { createPinia } from 'pinia'
const store = createPinia()
export default store
在 main.ts 中引入并使用
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import store from './store'
const app = createApp(App)
app.use(store)
定义 State
- 在 src/store 下面创建一个 user.ts
//src/store/user.ts
import { defineStore } from 'pinia'
import { useAppStore } from './app'
export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
name: '张三',
age: 18
}
},
getters: {
fullName: (state) => {
return state.name + '丰'
}
},
actions: {
updateState(data: any) {
this.$state = data
this.updateAppConfig()
},
updateAppConfig() {
const appStore = useAppStore()
appStore.setData('app-update')
}
}
})
//src/store/app.ts
import { defineStore } from 'pinia'
export const useAppStore = defineStore({
id: 'app',
state: () => {
return {
config: 'app'
}
},
actions: {
setData(data: any) {
console.log(data)
this.config = data
}
}
})
获取/更新 State
<script setup lang="ts">
import { useUserStore } from '@/store/user'
import { useAppStore } from '@/store/app'
import { storeToRefs } from 'pinia'
import { computed } from 'vue'
const userStore = useUserStore()
const appStore = useAppStore()
console.log(appStore.config)
console.log(userStore)
console.log(userStore.name)
const name = computed(() => userStore.name)
const { age } = storeToRefs(userStore)
const updateUserState = () => {
const { name, age } = userStore.$state
userStore.updateState({
name: name + 1,
age: age + 1
})
}
</script>
<template>
<div>姓名:{{ name }}</div>
<div>年龄:{{ age }}</div>
<div>计算的名字:{{ userStore.fullName }}</div>
<div>app的config: {{ appStore.config }}</div>
<button @click="updateUserState">更新数据</button>
</template>
<style lang="scss" scoped></style>
数据持久化
-
插件 pinia-plugin-persistedstate 可以辅助实现数据持久化功能。
-
数据默认存在 sessionStorage 里,并且会以 store 的 id 作为 key。
-
安装依赖
pnpm i pinia-plugin-persistedstate
- 引用插件
// src/store/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const store = createPinia()
store.use(piniaPluginPersistedstate)
export default store
- 在对应的 store 里开启 persist 即可
export const useUserStore = defineStore({
id: 'user',
state: () => {
return {
name: '张三'
}
},
// 开启数据缓存
persist: {
key: 'user',
storage: sessionStorage, // 数据存储位置,默认为 localStorage
paths: ['name'], // 用于部分持久化状态的点表示法路径数组,表示不会持久化任何状态(默认为并保留整个状态)
overwrite: true
}
})
✅ Eslint + Prettier 统一开发规范
1. 安装依赖
pnpm i -D eslint eslint-plugin-vue prettier @vue/eslint-config-prettier @vue/eslint-config-typescript @rushstack/eslint-patch
2. 编写相关文件
- .eslintrc.js
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
env: {
'vue/setup-compiler-macros': true
},
parserOptions: {
ecmaVersion: 12
},
rules: {
'prettier/prettier': 'warn',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off'
}
}
- .prettierc.js
module.exports = {
// 定制格式化要求
overrides: [
{
files: '.prettierrc',
options: {
parser: 'json'
}
}
],
printWidth: 100, // 一行最多 100 字符
tabWidth: 2, // 使用 4 个空格缩进
semi: false, // 行尾需要有分号
singleQuote: true, // 使用单引号而不是双引号
useTabs: false, // 用制表符而不是空格缩进行
quoteProps: 'as-needed', // 仅在需要时在对象属性两边添加引号
jsxSingleQuote: false, // 在 JSX 中使用单引号而不是双引号
trailingComma: 'none', // 末尾不需要逗号
bracketSpacing: true, // 大括号内的首尾需要空格
bracketSameLine: false, // 将多行 HTML(HTML、JSX、Vue、Angular)元素反尖括号需要换行
arrowParens: 'always', // 箭头函数,只有一个参数的时候,也需要括号 avoid
rangeStart: 0, // 每个文件格式化的范围是开头-结束
rangeEnd: Infinity, // 每个文件格式化的范围是文件的全部内容
requirePragma: false, // 不需要写文件开头的 @prettier
insertPragma: false, // 不需要自动在文件开头插入 @prettier
proseWrap: 'preserve', // 使用默认的折行标准 always
htmlWhitespaceSensitivity: 'css', // 根据显示样式决定 html 要不要折行
vueIndentScriptAndStyle: false, //(默认值)对于 .vue 文件,不缩进 <script> 和 <style> 里的内容
endOfLine: 'lf', // 换行符使用 lf 在Linux和macOS以及git存储库内部通用\n
embeddedLanguageFormatting: 'auto' //(默认值)允许自动格式化内嵌的代码块
}
- .vscode/settings.json
{
"editor.formatOnSave": false, // 每次保存的时候自动格式化
"editor.formatOnPaste": true, // 自动格式化粘贴内容
"editor.tabCompletion": "on", // tab 自动补全
"editor.codeActionsOnSave": { // 保存时使用 ESLint 修复可修复错误
"source.fixAll": true,
"source.fixAll.eslint": true, // 保存时使用 ESLint 修复可修复错误
// "source.fixAll.stylelint": true
},
// 文件设置
"files.eol": "\n", // 默认行尾字符。 git全局配置 git config --global core.autocrlf false
// eslint 设置
"eslint.alwaysShowStatus": true, // 总是在 VSCode 显示 ESLint 的状态
"eslint.probe": [ // eslint 校验的语言类型 - 新版
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"html",
"vue",
"markdown",
"tsx"
],
}
✅ husky + lint-staged 提交校验
1. 安装依赖
pnpm i -D husky lint-staged
2. 添加脚本命令
npm set-script prepare "husky install" // 在 package.json/scripts 中添加 "prepare": "husky install" 命令, 这个命令只在linux/uinx系统有效,win系统可以直接在scripts中添加命令
npm run prepare // 初始化husky,将 git hooks 钩子交由,husky执行, 会在根目录创建 .husky 文件夹
npx husky add .husky/pre-commit "npx lint-staged" // pre-commit 执行 npx lint-staged 指令
3. 创建 .lintstagedrc.json
{
"**/*.{js,ts,tsx,jsx,vue,scss,css}": [
"prettier --write \"src/**/*.ts\" \"src/**/*.vue\"",
"eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
]
}
✅ 使用 Mock 数据
- 文档:github.com/vbenjs/vite…
- mock 数据目前测试,在开发环境 XHR 和 fetch 都生效,生产环境只能使用 XHR 类型请求库调用,fetch 不生效
1. 安装依赖
pnpm i -D vite-plugin-mock
# 如果不使用mockjs,则不需要安装 mockjs 相关依赖
pnpm i mockjs
pnpm i -D @types/mockjs
2. 生产环境 相关封装
// mock/_createProductionServer.ts
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
const modules = import.meta.globEager('./**/*.ts')
const mockModules: any[] = []
Object.keys(modules).forEach((key) => {
if (key.includes('/_')) {
return
}
mockModules.push(...modules[key].default)
})
/**
* Used in a production environment. Need to manually import all modules
*/
export function setupProdMockServer() {
createProdMockServer(mockModules)
}
// mock/_util.ts
// Interface data format used to return a unified format
import { Recordable } from 'vite-plugin-mock'
export function resultSuccess<T = Recordable>(result: T, { message = 'ok' } = {}) {
return {
code: 0,
result,
message,
type: 'success'
}
}
export function resultPageSuccess<T = any>(
page: number,
pageSize: number,
list: T[],
{ message = 'ok' } = {}
) {
const pageData = pagination(page, pageSize, list)
return {
...resultSuccess({
items: pageData,
total: list.length
}),
message
}
}
export function resultError(message = 'Request failed', { code = -1, result = null } = {}) {
return {
code,
result,
message,
type: 'error'
}
}
export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]): T[] {
const offset = (pageNo - 1) * Number(pageSize)
const ret =
offset + Number(pageSize) >= array.length
? array.slice(offset, array.length)
: array.slice(offset, offset + Number(pageSize))
return ret
}
export interface requestParams {
method: string
body: any
headers?: { authorization?: string }
query: any
}
/**
* @description 本函数用于从request数据中获取token,请根据项目的实际情况修改
*
*/
export function getRequestToken({ headers }: requestParams): string | undefined {
return headers?.authorization
}
// mock/sys/user
import { MockMethod } from 'vite-plugin-mock'
import { resultError, resultSuccess, getRequestToken, requestParams } from '../_util'
export default [
{
url: '/basic-api/getUserInfo',
method: 'get',
response: (request: requestParams) => {
console.log('----请求了getUserInfo---')
return resultSuccess({
name: '章三',
age: 40,
sex: '男'
})
}
}
] as MockMethod[]
3. 修改 vite.config.ts 配置
export default ({ mode, command }: ConfigEnv): UserConfigExport => {
const isBuild = command === 'build'
return defineConfig({
plugins: [
viteMockServe({
ignore: /^_/, // 正则匹配忽略的文件
mockPath: 'mock', // 设置mock.ts 文件的存储文件夹
localEnabled: true, // 设置是否启用本地 xxx.ts 文件,不要在生产环境中打开它.设置为 false 将禁用 mock 功能
prodEnabled: true, // 设置生产环境是否启用 mock 功能
watchFiles: true, // 设置是否监视mockPath对应的文件夹内文件中的更改
// 代码注入
injectCode: `
import { setupProdMockServer } from '../mock/_createProductionServer';
setupProdMockServer();
`
})
]
})
}
- 这样我们一个基础的项目模版做好了,不管是想改造成h5项目还是pc端项目都可以。
- github地址:ynzy/vite-vue3-template: 🎉基于 vite2 + Vue3.2 + TypeScript + pinia + mock + sass 的基础模版 (github.com)