# vue3-pinia-quick-start **Repository Path**: malguy/vue3-pinia-quick-start ## Basic Information - **Project Name**: vue3-pinia-quick-start - **Description**: vue quick start - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-10-16 - **Last Updated**: 2024-08-14 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # vue quick start ## init vue project ```shell npm init vue@latest ``` ## tailwind support ```shell npm i -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` ```js // tailwind.config.js /** @type {import('tailwindcss').Config} */ module.exports = { content: ["./interceptor.js.html", "./src/**/*.{vue,js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], }; ``` ```js // .eslintrc.cjs /* eslint-env node */ require("@rushstack/eslint-patch/modern-module-resolution"); module.exports = { // ... env: { node: true, }, }; ``` ```css /* base.css */ @tailwind base; @tailwind components; @tailwind utilities; ``` ```js // main.js import "./assets/base.css"; // ... ``` # pinia ```js import { defineStore } from "pinia"; // 定义状态 export default defineStore("modal", { state: () => ({ isOpen: true, }), getters: { // 类似计算属性 // 根据isOpen返回string hiddenClass(state) { return !state.isOpen ? "hidden" : ""; }, }, }); ``` ```vue ``` ```vue ``` # form validate ```shell npm i vee-validate ``` ```js // main.js // ... import VeeValidatePlugin from "./includes/validation"; // ... app.use(VeeValidatePlugin); app.mount("#app"); ``` ```js // src/includes/validation.js import { configure, Form as VeeForm, Field as VeeField, defineRule, ErrorMessage, } from "vee-validate"; import { required, not_one_of as excluded, confirmed, min_value as minVal, max_value as maxVal, min, max, alpha_spaces as alphaSpaces, email, } from "@vee-validate/rules"; export default { // app.use(x,{y: 1}): x->app y->1 // install(app, options) { install(app) { app.component("VeeForm", VeeForm); app.component("VeeField", VeeField); app.component("ErrorMessage", ErrorMessage); // 全局注册规则 defineRule("required", required); defineRule("tos", required); defineRule("min", min); defineRule("max", max); defineRule("alpha_spaces", alphaSpaces); defineRule("email", email); defineRule("min_value", minVal); defineRule("max_value", maxVal); defineRule("confirmed", confirmed); defineRule("excluded", excluded); defineRule("country_excluded", excluded); configure({ generateMessage: (ctx) => { const messages = { required: `The field ${ctx.field} is required`, min: `The field ${ctx.field} is too short`, max: `The field ${ctx.field} is too long`, alpha_spaces: `The field ${ctx.field} may only contain alphabetical characters and spaces`, email: `The field ${ctx.field} must be a valid email`, min_value: `The field ${ctx.field} is too low`, max_value: `The field ${ctx.field} is too high`, excluded: `You are not allowed to use this value for the field ${ctx.field}`, country_excluded: `Due to restrctions, we do not accept users from this location`, passwords_mismatch: `The passwords don't match`, tos: `You must accept the Terms of Service`, }; const message = messages[ctx.rule.name] ? messages[ctx.rule.name] : `The field ${ctx.field} is invalid`; return message; }, }); }, }; ``` ```shell npm i @vee-validate/rules ``` ```html ``` > use action to register or login ```js // stores/user.js import { defineStore } from "pinia"; export default defineStore("user", { state: () => ({ //用户是否登录 userLoggedIn: false, }), actions: { async register(values) { // await auth.register(values.email, values.password); console.log(values) this.userLoggedIn = true; }, async login(values) { // 请求后端 // await auth.login(values.email, values.password); console.log(values) this.userLoggedIn = true; }, }, }) ``` # axios ```shell npm i axios ``` # router ```js // main.js import "./assets/base.css"; import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app') ``` ```js // router/interceptor.js.js import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import('../views/AboutView.vue') }, { path: '/login', name: 'login', component: () => import('../views/LoginView.vue') }, { path: '/404', name: '404', component: () => import('../views/404.vue'), beforeEnter: (to, from, next) => { console.log('Router Guard') console.log(to, from) next() } }, { path: '/:catchAll(.*)*', // 上面的都匹配不到,就重定向到404 redirect: { name: '404' } } ] }) // 全局路由守卫 router.beforeEach((to, from, next) => { console.log('Global Guard') console.log(to, from) next() }) export default router ``` ## router guard ```js // router/interceptor.js.js import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ // ... { path: '/404', name: '404', component: () => import('../views/404.vue'), beforeEnter: (to, from, next) => { console.log('Router Guard') console.log(to, from) next() } }, { path: '/:catchAll(.*)*', // 上面的都匹配不到,就重定向到404 redirect: { name: '404' } } ] }) // 全局路由守卫 router.beforeEach((to, from, next) => { console.log('Global Guard') console.log(to, from) next() }) export default router ``` ```html ``` ## Route Meta Fields ```html ``` ```js import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import('../views/AboutView.vue'), // 定义meta数据 meta: { // 是否需要登录访问 requiresAuth: true } } ] }) // ... export default router ``` ```html ``` # file upload component ```html ``` ```html ``` # Internationalization 国际化 ## install ```shell npm i vue-i18n@9 ``` ## includes/i18n.js ```js import { createI18n } from "vue-i18n"; import en from "@/locales/en.json"; import cn from "@/locales/cn.json"; export default createI18n({ locale: "en", fallbackLocale: "en", messages: { en, cn, }, }); ``` ## main.js ```js const app = createApp(App) // ... app.use(i18n) app.mount('#app') ``` ## locales/en.json ```json { "error": { "nofound": "404 NOT FOUND!" }, "info": { "translate": "switch to cn" } } ``` ## includes/cn.json ```json { "error": { "nofound": "页面跑丢了~!" }, "info": { "translate": "切换为英文" } } ``` ## app.vue ```html ``` # PWA ```shell npm i vite-plugin-pwa -D ``` ```js // vite.config.js import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { VitePWA } from "vite-plugin-pwa"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [ vue(), VitePWA({ registerType: 'autoUpdate', devOptions: { // 生成清单文件 enabled: true }, manifest: { name: "vue-quick-start", theme_color: '#ff5e3a', icons: [ { src: 'assets/logo.png', size: '192x192', type: 'image/png' } ] }, workbox: { globPatterns: ['**/*.{js,css,html,png,jpg}'] } }) ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } }) ``` ![](README/438dc912.png) ```shell npm run build ``` # Progress Bar 加载进度条 ```shell npm i nprogress ``` ```js // main.js import 'nprogress/nprogress.css' import progressBar from "./includes/progress-bar"; progressBar(router) ``` # 优化 ## 自动导出组件 ```js // includes/_globals.js // 自动导出组件 export default { install(app) { const baseComponents = import.meta.glob("../components/base/*.vue", { eager: true, }); Object.entries(baseComponents).forEach(([path, module]) => { const componentName = _.upperFirst( _.camelCase( path .split("/") .pop() .replace(/\.\w+$/, "") ) ); // ../components/base/Button.vue Button // console.log(path, componentName); // export default app.component(`Base${componentName}`, module.default); }); }, } ``` ## 让组件和数据一同加载 > song.vue 的 created 用 beforeRouterEnter 替代,这是为了在低速机型上,让组件和数据一同加载,而不是组件加载完又慢慢填数据 ```js ``` ## 动态路由导入 > 动态路由导入,可以减少包的大小 > 在 home 页面,其他组件也默认加载,但是我们却没有使用其他组件,所以浪费了性能 > 打包时,所有组件被打包到一个文件,我们可以使用 chunks,只有被用到才加载到 bundle(打包后的文件) ![](README/c793422f.png) ## 进度条 > chunk 的缺点之一是页面之间的加载时间更长,因为没有一开始加载所有组件,所以某页面第一次跳转时需要向服务器下载需要的组件(后续再跳转用的是缓存里的组件) > 优雅的解决方式是添加加载进度条 ```shell npm i nprogress ``` ```js // includes/progress-bar.js import NProgress from "nprogress"; export default (router) => { router.beforeEach((to, from, next) => { NProgress.start(); next(); }); router.afterEach(NProgress.done); }; ``` ## 006 Code Coverage > 使用谷歌开发者工具检测代码覆盖率,从而决定哪些组件应该被动态导入,哪些不用 ![](README/036f9498.png) ![](README/925f51d6.png) > 红色表示未使用的代码 ![](README/9536d3ee.png) ![](README/6f94690e.png) ## 007 Rollup Visualizer ```shell npm i -D rollup-plugin-visualizer ``` ![](README/9ce647e1.png) > 该插件可以可视化打包后文件的大小分布情况 ```shell npm run build ``` ![](README/6fbeef12.png) ![](README/1db766ae.png) > 根据树状图优化使用了 lodash 的地方,改为导入特定函数,这样可以减少导入的内容 # test # composition api > 组合式 api 适合结合 ts > 组合式适合构筑大型组件,将相同逻辑结合在一块区域(类似 react hook?) ## 002 Mixins ![](20230711154648.png) ```html // app.vue ``` ```js // mixin.js export default { data() { return { offset: 0, }; }, mounted() { console.log("mixin mounted"); window.addEventListener("scroll", this.update); }, methods: { update() { this.offset = window.pageYOffset; }, }, }; ``` > mixin 会互相覆盖,但是不会提示你,所以当使用多个 mixin,就得注意命名问题,组合 api 可以解决 ## 003 Reactive References > vue 的组合式 api 定义的变量默认不是响应式的,我们需要将其定义为响应式,否则不能动态更新视图(值可以变,但不会重新渲染) ```html // app.vue ``` ## 004 The Reactive Function ref可以基础类型和对象类型, reactive只能对象类型 ```html // app.vue ``` ## 005 Watchers and Computed Properties ```js // app.vue ``` ## 006 Lifecycle Functions ![](20230711170026.png) ![](20230711185205.png) ```html // app.vue ``` ## 007 Props ```html // components/alert.vue ``` ## 008 Template Refs ```html ``` ## 009 Emitting Events ![](20230711192854.png) ## 010 Advantages of the Composition API ![](20230711192911.png) ![](20230711193957.png) ```html // app.vue ``` ```js // hooks/number.js import { computed, ref } from "vue"; export const useNumber = () => { let num = ref(0); function increment() { num.value++; console.log(num); } const double = computed(() => { return num.value * 2; }); return { num, increment, double }; }; ``` ```js // hooks/phrase.js import { ref, watchEffect } from "vue"; export const usePhrase = () => { const phrase = ref(""); const reversedPhrase = ref(""); const num = ref(""); watchEffect(() => { reversedPhrase.value = phrase.value.split("").reverse().join(""); }); return { phrase, reversedPhrase, num }; }; ``` ## 011 Router Hooks ```html // views/about.vue ``` ```html // app.vue ``` ![](20230711201405.png) ## 012 Pinia Hooks ```js // stores/counter.js import { defineStore } from "pinia"; export const useCounterStore = defineStore({ id: "counter", state: () => ({ counter: 0, }), getters: { doubleCount: (state) => state.counter * 2, }, actions: { increment() { this.counter++; }, }, }); ``` ```html // views/about.vue ``` ![](20230711203326.png) ## 013 Verifying Reactivity ```html // views/home.vue ``` ```js // hooks/number.js import { computed, ref, isRef, isReactive, reactive } from "vue"; export const useNumber = () => { let num = ref(0); const accounts = reactive({ checking: 3242, savings: 242, }); // 判断是不是响应式 console.log(isRef(num)); console.log(isReactive(accounts)); function increment() { num.value++; console.log(num); } const double = computed(() => { return num.value * 2; }); return { num, increment, double }; }; ``` ![](20230711204913.png) ## 014 The setup Attribute ```html // app.vue ``` # 22 - Component Design Patterns ## 001 Section Overview ![](20230711205428.png) ![](20230711205439.png) ## 002 Controlled Components > 受控组件就是自己没有状态,而是由父组件提供,并且更新值是触发父类的值的更新 ![](20230712055406.png) ```js // components/EmailInput.vue ``` ```js // app.vue ``` > v-model 手动指定传递的变量名 ![](20230712063513.png) ![](20230712063535.png) ## 003 Separation of Concerns ![](20230712063609.png) ![](20230712063647.png) > 解耦 ui 和业务逻辑,这样重写 ui 或逻辑时不会大改 ![](20230712064242.png) ```js // validate-email.js export default function validateEmail(email) { return email.length > 0 && email.length >= 4; } ``` ## 004 Third-Party Libraries as Controlled Components > 将不支持 vue 的第三方库(可能是原生 js 写的),转为 vue 受控组件,然后整合进项目 ![](20230712064323.png) ```shell npm i @joeattardi/emoji-button ``` ```js // app.vue ``` ```js ``` ## 005 Moving Beyond Vue’s Event System > 不使用 vue 提供的 api 来开发 ![](20230712065623.png) > 用 vue 的方式获取 dom 并自动聚焦 ```js // components/modal.vue ``` > 原生 js ```js ``` ## 006 Encapsulating Scrolling > 在有上层元素显示时禁止背景的滚动 ```js // modal.vue ``` ```js // app.vue ``` ## 007 The Teleport Component ![](20230712083522.png) ```js // action.vue ``` ```js // app.vue ``` > 这种方式不易拓展 ![](20230712083828.png) > 如果子组件还有子组件,最底层的子组件要更新顶层的 app 的状态,需要传递多次 emit ![](20230712083939.png) ![](20230712084028.png) > 使用 action 组件 ![](20230712084049.png) ```js // app.vue ``` ```js ``` > 解决可能出现的 action 样式污染 ![](20230712084305.png) ```js ``` #