【フロントエンドエンジニアリング】vite徹底解説(2) ~vue3系バレル+tsでポスト管理システムを構築~

基本パッケージをインストールする

npm create vite@latest
# 这里选择的是Vue+Typescript的组合
cd vue-admin
npm install

# 先安装基础包
npm install vue-router@4
npm i pinia
npm i axios
npm install sass --save-dev
npm install element-plus --save
npm install @element-plus/icons-vue
npm install -D unplugin-vue-components unplugin-auto-import
npm i eslint -D

# 提交规范
npm i lint-staged husky  --save-dev
npm install @commitlint/cli @commitlint/config-conventional -D

コード仕様

npm init @eslint/config

たくさんのプロンプトが表示されます。選択肢は次のとおりです。

Need to install the following packages:
  @eslint/create-config
Ok to proceed? (y)
√ How would you like to use ESLint? · style       
√ What type of modules does your project use? · esm
√ Which framework does your project use? · vue
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ How would you like to define a style for your project? · guide
√ Which style guide do you want to follow? · standard-with-typescript
√ What format do you want your config file to be in? · JavaScript
Checking peerDependencies of eslint-config-standard-with-typescript@latest
The config that you've selected requires the following dependencies:

eslint-plugin-vue@latest eslint-config-standard-with-typescript@latest @typescript-eslint/eslint-plugin@^5.50.0 eslint@^8.0.1 eslint-plugin-import@^2.25.2 eslint-plugin-n@^15.0.0 eslint-plugin-promise@^6.0.0 typescript@*
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · npm
Installing eslint-plugin-vue@latest, eslint-config-standard-with-typescript@latest, @typescript-eslint/eslint-plugin@^5.50.0, eslint@^8.0.1, eslint-plugin-import@^2.25.2, eslint-plugin-n@^15.0.0, eslint-plugin-promise@^6.0.0, typescript@*

ファイルがプロジェクト内に生成され.eslintrc.cjs、それを検証するようにスクリプトを構成します。

"lint": "eslint src/**/*.{js,jsx,vue,ts,tsx} --fix"

ただし、実行時にエラーが報告されました。現在サポートされている"typescript": "^5.1.3",バージョン範囲は:なので、ダウングレードする必要があります: 。eslint を構成する途中で多くの問題があり、解決策が直接提供されます。@typescript-eslint/typescript-estreets=3.3.1 <5.1.0[email protected]

1 つ目は、以下を変更することです.eslintrc.cjs

module.exports = {
    
    
  env: {
    
    
    browser: true,
    es2021: true
  },
  extends: [
    'plugin:vue/vue3-essential',
    'standard-with-typescript'
  ],
  parser: "vue-eslint-parser",
  overrides: [
  ],
  parserOptions: {
    
    
    ecmaVersion: 'latest',
    sourceType: 'module',
    project: ["./tsconfig.json"],
    parser: "@typescript-eslint/parser",
    extraFileExtensions: ['.vue']
  },
  plugins: [
    'vue'
  ],
  rules: {
    
    
    'space-before-function-paren': [2, {
    
    
      anonymous: 'always',
      named: 'never',
      asyncArrow: 'always'
    }],
    'vue/multi-word-component-names': 0,
    "space-before-function-paren": 0,
    "@typescript-eslint/consistent-type-assertions": 0,
    "@typescript-eslint/ban-types": [
      "error",
      {
    
    
        "extendDefaults": true,
        "types": {
    
    
          "{}": false
        }
      }
    ]
  }
}

vite-env.d.tsチェックを無視するには、コメント行を追加します。

// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference types="vite/client" />

構成で発生した問題についてはeslint、この偉人が書いた文書を参照して詳細を確認してください: Eslint: eslint (標準ルール) を vue3 プロジェクトに追加します。

コミット仕様

git init

次のコードを に追加しpackage.json、それを使用して eslint と stylelint を呼び出し、一時記憶領域内のコードを確認します。

"lint-staged": {
    
    
    "*.{vue,js}": [
      "npm run lint"
    ]
  }

埋め込む:

npm pkg set scripts.postinstall="husky install"
# 等同于执行npm i,执行过程中会生成.husky文件夹
npm run postinstall

npx husky add .husky/pre-commit "npm lint"
git add .husky/pre-commit

このようにすると、実行git commit時に自動的に実行されますnpm lint

実行中のプロセス中に、ノードが内部コマンドまたは外部コマンドではないというエラーが報告されました。問題はありません。おそらく nvm ツールの問題であるため、 node のバージョン管理を行うためにvolta をnode -v後で変更しました。

npx husky add .husky/commit-msg "npx --no -- commitlint --edit $1"

新しいcommitlint.config.cjs:

module.exports = {
    
    
  extends: ['@commitlint/config-conventional'],
  rules: {
    
    
    'type-enum': [2, 'always', [
      'feat', // 新增功能
      'update', // 更新功能
      'ui', // 样式改动
      'fix', // 修复功能bug
      'merge', // 合并分支
      'refactor', // 重构功能
      'perf', // 性能优化
      'revert', // 回退提交
      'style', // 不影响程序逻辑的代码修改(修改空白字符,格式缩进,补全缺失的分号等)
      'build', // 修改项目构建工具(例如 glup,webpack,rollup 的配置等)的提交
      'docs', // 文档新增、改动
      'test', // 增加测试、修改测试
      'chore' // 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
    ]],
    'scope-empty': [0],
    // 'scope-empty': [2, 'never'], 作用域不为空
    'scope-case': [0],
    'subject-full-stop': [0],
    'subject-case': [0]
  }
}

変更tsconfig.json:

"include": [
  //...
  "commitlint.config.cjs"
 ],

変更.eslintrc.cjs:

project: ["./tsconfig.json", "./commitlint.config.cjs"],
git add .
# 失败
git commit -m "commit校验"
# 成功
git commit -m "feat: commit校验"

パスのエイリアスを設定する

import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
    
    
  plugins: [vue()],
  resolve: {
    
    
    alias: {
    
    
      '@': resolve('src')
    },
    // 顺便把可以省略的后缀配置一下,在vite中不支持省略.vue
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  }
})

変更tsconfig.jsonおよび追加:

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

リセットスタイル

次の下にassets新しいフォルダーを作成しますstyles/reset.css

/**
 * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/)
 * http://cssreset.com
 */
 
 html, body, div, span, applet, object, iframe,
 h1, h2, h3, h4, h5, h6, p, blockquote, pre,
 a, abbr, acronym, address, big, cite, code,
 del, dfn, em, img, ins, kbd, q, s, samp,
 small, strike, strong, sub, sup, tt, var,
 b, u, i, center,
 dl, dt, dd, ol, ul, li,
 fieldset, form, label, legend,
 table, caption, tbody, tfoot, thead, tr, th, td,
 article, aside, canvas, details, embed, 
 figure, figcaption, footer, header, hgroup, 
 menu, nav, output, ruby, section, summary,
 time, mark, audio, video{
    
    
   margin: 0;
   padding: 0;
   border: 0;
   font-size: 100%;
   font: inherit;
   font-weight: normal;
   vertical-align: baseline;
 }
 /* HTML5 display-role reset for older browsers */
 article, aside, details, figcaption, figure, 
 footer, header, hgroup, menu, nav, section{
    
    
   display: block;
 }
 ol, ul, li{
    
    
   list-style: none;
 }
 blockquote, q{
    
    
   quotes: none;
 }
 blockquote:before, blockquote:after,
 q:before, q:after{
    
    
   content: '';
   content: none;
 }
 table{
    
    
   border-collapse: collapse;
   border-spacing: 0;
 }
  
 /* custom */
 a{
    
    
   color: #7e8c8d;
   text-decoration: none;
   backface-visibility: hidden;
   -webkit-backface-visibility: hidden;
 }
 ::-webkit-scrollbar{
    
    
   width: 5px;
   height: 5px;
 }
 ::-webkit-scrollbar-track-piece{
    
    
   background-color: rgba(0, 0, 0, 0.2);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:vertical{
    
    
   height: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 ::-webkit-scrollbar-thumb:horizontal{
    
    
   width: 5px;
   background-color: rgba(125, 125, 125, 0.7);
   border-radius: 6px;
   -webkit-border-radius: 6px;
 }
 html, body{
    
    
   width: 100%;
   font-family: "Arial", "Microsoft YaHei", "黑体", "宋体", "微软雅黑", sans-serif;
 }
 body{
    
    
   line-height: 1;
   -webkit-text-size-adjust: none;
   -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
 }
 html{
    
    
   overflow-y: scroll;
 }
  
 /*清除浮动*/
 .clearfix:before,
 .clearfix:after{
    
    
   content: " ";
   display: inline-block;
   height: 0;
   clear: both;
   visibility: hidden;
 }
 .clearfix{
    
    
   *zoom: 1;
 }
  
 /*隐藏*/
 .dn{
    
    
   display: none;
 }
 

SSSを使用する

.scssVite は、、、、およびファイル組み込みサポートを提供し.sassますそれらに特定のプラグインをインストールする必要はありませんが、対応するプリプロセッサの依存関係をインストールする必要があります。.less.styl.stylusVite

通常、プロジェクト内でいくつかのテーマカラーを定義します。

// variable.scss
$font-color-gray:rgb(147,147,147);

または、いくつかのカプセル化されたコレクション スタイル:

// mixins.scss
@mixin line-clamp($lines) {
    
    
  word-break: break-all;
  display: -webkit-box;
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: $lines;
  -webkit-box-orient: vertical;
}

@mixin ellipsis() {
    
    
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}


次に、次vite.config.jsのように設定します。

css: {
    
    
    preprocessorOptions: {
    
    
      scss: {
    
    
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }

ディレクトリを削除しstyle.css、新しいディレクトリを作成しますstyles/common.scss

// common.scss
@import url('./reset.css');

次に、次のようmain.tsにインポートします。

import '@/assets/styles/common.scss'

このようにして、グローバル スタイルが初期化されます。次に、合計が機能するかどうかをテストしvariable.scss灰色になっていて 2 行が省略されているかどうかを確認するためにmixins.scss変更します。HelloWorld.vue

<script setup lang="ts">
</script>

<template>
  <div class="box">没错,这里要使用浏览器的获取媒体设备的 api 来拿到摄像头的视频流,设置到 video 上,然后对 video 做下镜像反转,加点模糊就好了。</div>
</template>

<style scoped lang="scss">
.box {
      
      
  color: $font-color-gray;
  height: 40px;
  line-height: 20px;
  width: 200px;
  @include line-clamp(2);
}
</style>

ルーティングを構成する

次にルーティングを設定すると、次の結果が得られると思います。

- 登录页
- 带菜单栏的框架
  - 主页
  - 人员管理
    - 客户管理
    - 员工管理
- 404

したがって、次のファイルを作成します。

ここに画像の説明を挿入

このプロセスでは、いくつかの問題が発生します。

  • ルートを記述するときは、サフィックスが必要なコンポーネントを導入します.vue
  • import xxx from '@/xxx'上記のように合計値tsconfg.jsonを設定していないため、エラーが報告されます。baseUrlpaths

layoutサブルートはファイル内でネストされるため、layout以下を追加する必要がありますrouter-view

<template>
  <div>布局</div>
  <router-view></router-view>
</template>

他のファイルは次のように記述するだけです。

<template>
  <p>主页</p>
</template>

新しいrouterフォルダー:

- router
  -hooks # 后期做登录校验和鉴权用的
  - routes
    - index.ts # 总输出文件
    - others.ts # 不需要layout这一层的路由均可以放在这里
    - person.ts # 人员管理模块
  - index.ts   # 总输出文件

各ファイルの内容は次のとおりです。

// person.ts
export default [
  {
    
    
    path: '/person',
    name: 'Person',
    meta: {
    
     title: '人员管理' },
    redirect: '/person/customer',
    children: [
      {
    
    
        path: '/person/customer',
        name: 'PersonCustomer',
        meta: {
    
     title: '客户管理' },
        component: () => import('@/views/person/customer/index.vue')
      },
      {
    
    
        path: '/person/staff',
        name: 'PersonStaff',
        meta: {
    
     title: '员工管理' },
        component: () => import('@/views/person/staff/index.vue')
      }
    ]
  }
];

// others.ts
export default [
  {
    
    
    path: '/login',
    name: 'Login',
    meta: {
    
     title: '登录' },
    component: () => import('@/views/login/index.vue')
  }
];

// router/routes/index.ts
import Layout from '@/views/layout/index.vue';
import personRoutes from './person';
import otherRoutes from './others';

export default [
  {
    
    
    path: '/',
    name: 'Layout',
    component: Layout,
    children: [
      {
    
    
        path: '/',
        name: 'Index',
        meta: {
    
     title: '主页' },
        component: () => import('@/views/index/index.vue')
      },

      ...personRoutes,
    ]
  },
  ...otherRoutes,
  {
    
    
    path: '/404',
    name: 'NotFound',
    meta: {
    
     title: '404' },
    component: () => import('@/views/404/index.vue')
  },
  {
    
    
    path: "/:pathMatch(.*)",
    redirect: "/404",
    name:'ErrorPage',
    meta: {
    
     title: '' },
  }
];

最初にフォルダーを脇に置きhooks、単に次のように記述しますindex.ts

// router/index.ts
import routes from "./routes";
export default routes;

新しいものを作成しますsrc/plugins/index.ts。以前にコンテンツを登録したとき、それを main.ts に直接配置しましたが、これは保守が容易ではないため、将来的にはここに一律にマウントします。

// src/plugins/index.ts
import {
    
     createRouter, createWebHashHistory } from 'vue-router';
import routes from '@/router/index';

export default (app: any) => {
    
    
  // 注册路由
  const router = createRouter({
    
    
    history: createWebHashHistory(),
    routes
  })

  app.use(router);
}

変更することを忘れないでくださいApp.vue:

<template>
  <router-view></router-view>
</template>
import {
    
     createApp } from 'vue'
import '@/assets/styles/common.scss'
import App from './App.vue'
import installPlugins from '@/plugins';

const app = createApp(App);
installPlugins(app);
app.mount('#app')

これにより、以下のテストが可能になります。

http://127.0.0.1:5173/#/
http://127.0.0.1:5173/#/login
http://127.0.0.1:5173/#/person
http://127.0.0.1:5173/#/person/customer
http://127.0.0.1:5173/#/person/staff

構成プロセス中にエラーが見つかった場合: モジュール " xxx.vue" またはそれに対応する型宣言が見つからない場合は、次をvite-env.d.ts追加します。

declare module '*.vue' {
    
    
  import type {
    
     DefineComponent } from 'vue';
  const vueComponent: DefineComponent<{
    
    }, {
    
    }, any>;
  export default vueComponent;
} 

要素プラスを使用する

を使用する場合はVolartsconfig.jsonでグローバル コンポーネント タイプを指定しますcompilerOptions.type

// tsconfig.json
{
    
    
  "compilerOptions": {
    
    
    // ...
    // 然而这个配置在后期打包的时候报错了...
    "types": ["element-plus/global"]
  }
}

ここではオンデマンドインポートの方法を採用していますが、量を追求しない場合は完全インポートを使用することもできます。

// vite.config.ts
import {
    
     defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// 新增
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
    
     ElementPlusResolver } from 'unplugin-vue-components/resolvers'

const resolve = (dist) => path.resolve(__dirname, dist)

export default defineConfig({
    
    
  plugins: [
    vue(),
    // 新增
    AutoImport({
    
    
      resolvers: [ElementPlusResolver()]
    }),
    // 新增
    Components({
    
    
      resolvers: [ElementPlusResolver()]
    })
  ],
  resolve: {
    
    
    alias: {
    
    
      '@': resolve('./src')
    },
    extensions: [".js", ".ts", ".tsx", ".jsx"]
  },
  css: {
    
    
    preprocessorOptions: {
    
    
      scss: {
    
    
        additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
      }
    }
  }
})


element plus中国語の日付などのコンポーネントはデフォルトでは英語なので、コンポーネントを中国語に変更します。

ここに画像の説明を挿入

変更App.vue:

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

これは中国語で表示されます。

ここに画像の説明を挿入

導入した 2 つのプラグインについては、次のように説明します。

  • unplugin-vue-componentsVue テンプレートで使用されるコンポーネントを自動的に識別し、オンデマンドで自動的にインポートして登録するために使用されます。
  • unplugin-auto-importvite、webpack一般的に使用される構成ライブラリは、他の環境にオンデマンドで自動的にインポートできますAPI。たとえばVueref手動作業は必要ないimportため、構成ライブラリを構成し、いくつかの API の導入を削除できます。
export default defineConfig({
    
    
  plugins: [
    // ...
    AutoImport({
    
    
      imports: [
        'vue',
        'vue-router',
        'pinia'
      ],
      eslintrc: {
    
    
        enabled: true,
        filepath: './.eslintrc-auto-import.json',
        globalsPropValue: true
      },
      resolvers: [ElementPlusResolver()]
    }),
    // ...
  ],
})

保存が有効になると、auto-imports.d.tsコンテンツが自動的に入力され、.eslintrc-auto-import.jsoneslint グローバル変数構成がプロジェクトのルート ディレクトリに生成されます。

次に、変更してtsconfg.json次のようにします.eslintrc.cjs

// tsconfg.json
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "commitlint.config.cjs", "auto-imports.d.ts"],

// .eslintrc.cjs
project: ["./tsconfig.json", "./commitlint.config.cjs", './.eslintrc-auto-import.json'],

auto-imports.d.tsESLint 検証を無視する

# .eslintignore
auto-imports.d.ts

ここで注意する必要があります。

  1. Vue Router などのすべての API がcreateRouterインポートされるわけではありません。自動的にインポートできる特定の API については、unplugin-auto-import/src/presetsを参照してください。
  2. ファイルの生成.eslintrc-auto-import.json後に設定を追加する必要がない場合は、enabled: trueに設定することをお勧めしますfalse。そうしないと、このファイルが毎回生成されます。

設定後、ページ内の一部の参照を削除しても問題ないことがわかりました。

<script lang='ts' setup>
// import { storeToRefs } from 'pinia'
// import { useRouter } from 'vue-router'
// ...
</script>

コンポーネントをテストします。

<template>
  <p><el-button>测试</el-button></p>
</template>

これにより、ページ上にボタンが表示されます。

自動オンデマンド インポートの原則は、<template>識別に使用されるコンポーネントを自動的にインポートすることです。これは、JS でメソッドを直接呼び出すコンポーネントと同様です。ElMessageプラグインは自動インポートを認識して完了しないため、手動でインポートする必要があります (このような境界の問題を回避するために、完全なスタイル ファイルをインポートすることをお勧めします)。

変更しないvite-env.d.tsと、 ts に導入されたときにエラーが報告されますelement plus

declare module "element-plus";

中小規模でplugins/element-plus.ts試してください:

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import {
    
     ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

const options = {
    
    
  size: 'small',
  zIndex: 3000
}

const components = [
  ElLoading,
  ElMessage,
  ElMessageBox, 
  ElNotification
]

export default function install (app: any): void {
    
    
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    
    
    app.component(key, component)
  }

  components.forEach((component) => {
    
    
    app.use(component, options)
  })
}


// plugins/index.ts
export default (app: any) => {
    
    
  // ...

  // 注册element-plus
  installElementPlus(app);
}

テストを受けてください:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const onTest = () => {
      
      
  ElLoading.service({
      
       fullscreen: true });
}
</script>

グローバルプロパティ

以前の習慣によれば、loadingの呼び出しは確かに上記のメソッドではなく、グローバル にマウントされていますVue.prototypeが、今回のプロジェクトではオンデマンドインポートを使用しており、Vue3では書き方が変わっています。

app.config.globalProperties.$loading = ElLoading;
app.config.globalProperties.$message = ElMessage;
app.config.globalProperties.$msgBox = ElMessageBox;
app.config.globalProperties.$notification = ElNotification;

次に、使用の過程で次のようになります。

<script setup lang="ts">
const instance = getCurrentInstance()

onMounted(() => {
    
    
  instance.proxy.$message.success('setup - getCurrentInstance() 成功使用')
  // 也可以使用 appContext
  console.log(instance.appContext.config.globalProperties.$message === instance.proxy.$message) // true
})
</script>

しかし、公式ドキュメントを参照したところ、そのような方法はなくgetCurrentInstance、おそらく仕様を満たしていません。そこで、グローバルメソッドの注入を採用しましたprovide/inject

App.vue

<script setup lang="ts">
import locale from 'element-plus/lib/locale/lang/zh-cn'
import {
      
       ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

provide('$loading', ElLoading)
provide('$message', ElMessage)
provide('$messagebox', ElMessageBox)
provide('$notification', ElNotification)
</script>

<template>
  <el-config-provider :locale="locale">
    <router-view></router-view>
  </el-config-provider>
</template>

変更element-plus.ts:

import {
    
     App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// import { ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'
import 'element-plus/theme-chalk/index.css'

// const options = {
    
    
//   size: 'small',
//   zIndex: 3000
// }

// const components = [
//   ElLoading,
//   ElMessage,
//   ElMessageBox, 
//   ElNotification
// ]

export default function install (app: App): void {
    
    
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    
    
    app.component(key, component)
  }

  // components.forEach((component) => {
    
    
  //   app.use(component, options)
  // })
}

追加src/types/global.d.ts:

import {
    
     ElLoading, ElMessage, ElMessageBox, ElNotification } from 'element-plus'

export interface ComponentCustomProperties {
    
    
  $message: typeof ElMessage
  $msgBox: typeof ElMessageBox
  $loading: typeof ElLoading
  $notification: typeof ElNotification
}


テスト:

<template>
  <p><el-button @click="onTest">测试</el-button></p>
</template>

<script setup lang="ts">
const $loading = inject('$loading') as any 
const onTest = () => {
      
      
  $loading.service({
      
      
    lock: true,
    text: 'Loading',
  })
}
</script>

いくつかの一般的な TS 記述方法を確認する

開発サーバーとパッケージャーは、ts型チェックを行わずにファイルの構文エスケープのみを実行するため、使用時にvite開発サーバーtsが常に高速であることが保証されます。

一般的な例をいくつか示します。

<!-- 对props的类型声明和默认值 -->
<script setup lang="ts">
interface Props {
      
      
  msg?: string
  labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
      
      
   msg: 'hello',
   labels: () => ['one', 'two']
})
</script>


<!-- 另一种方式 -->
<script setup lang="ts">
interface Book {
      
      
  title?: string
  author: string
}
const props = defineProps({
      
      
  book: Object as PropType<Book>
})
</script>


<!-- 对emits进行声明 -->
<script setup lang="ts">
// 1.运行时
const emit = defineEmits(['change', 'update'])

// 2.基于类型
const emit = defineEmits<{
      
      
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()
</script>


<!-- 为computed指定返回类型 --->
<script setup lang="ts">
const double = computed<number>(() => {
      
       /** return number */ })
</script>

<!-- 为函数参数标注类型 -->
<script setup lang="ts">
const onClick = (e: Event) => {
      
      
  console.log((event.target as HTMLInputElement).value)
}
</script>

<!-- project和inject 然而我很少用到 -->
<script setup lang="ts">
import {
      
       provide, inject } from 'vue'
import type {
      
       InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>
provide(key, 'foo')

const foo = inject<string>('foo', 'bar')
</script>

<!-- 为模板引用标注类型 -->
<script setup lang="ts">
import {
      
       ref, onMounted } from 'vue'
const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
      
      
  el.value?.focus()
})
</script>

<template>
  <input ref="el" />
</template>


<!-- 为组件模板引用标注类型 -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
      
      
  modal.value?.open()
}
</script>

静的ページにログインする

コンポーネントはログイン ページで使用されますel-input。一般に、この種のフォーム コンポーネントの場合、ポスト管理の頻度は依然として非常に高いため、再パッケージ化して再利用する傾向があります。通常、その前後のスペースは最適化され、分解されますtextarea

<script lang='ts' setup>
import {
      
       computed } from 'vue';
const emits = defineEmits<{
      
      
  (e: 'update:value', value: string): void;
  (e: 'blur'): void;
  (e: 'focus'): void;
  (e: 'change', value: string): void;
  (e: 'clear'): void;
}>()

const props = defineProps({
      
      
  type: {
      
      
    type: String,
    default: 'string'
  },
  value: {
      
      
    type: [String, Number],
    default: '',
    required: true
  },
  maxlength: [String, Number],
  minlength: [String, Number],
  placeholder: {
      
      
    type: String,
    default: '请输入',
  },
  clearable: {
      
      
    type: Boolean,
    default: true
  },
  showPassword: {
      
      
    type: Boolean,
    default: false
  },
  disabled: {
      
      
    type: Boolean,
    default: false
  },
  prefixIcon: {
      
      
    type: String,
    default: ''
  },
  suffixIcon: {
      
      
    type: String,
    default: ''
  },
  inputStyle: [String, Object],
  showWordLimit: {
      
      
    type: Boolean,
    default: true
  },
  rows: Number,
  autosize: [Boolean, Object]
})

const input = computed({
      
      
  get(){
      
      
    console.log(props.value)
    return props.value;
  },

  set(val: any){
      
      
    if(typeof val === 'string') {
      
      
      val = val ? val.trim() : val;
    }
    emits('update:value', val);
  }
})

const onFocus = () => {
      
      
  emits('focus');
}

const onBlur = () => {
      
      
  emits('blur');
}

const onClear = () => {
      
      
  emits('clear');
}

const onChange = (val: string) => {
      
      
  emits('change', val);
}

</script>

<template>
  <el-input
    v-if="type === 'textarea'"
    v-model="input"
    :rows="rows"
    type="textarea"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :show-word-limit="showWordLimit"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :autosize="autosize"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @change="onChange"
  />
  <el-input 
    v-else
    v-model="input" 
    :type="type"
    :placeholder="placeholder"
    :maxlength="maxlength"
    :minlength="minlength"
    :clearable="clearable"
    :showPassword="showPassword"
    :disabled="disabled"
    :prefixIcon="prefixIcon"
    :suffixIcon="suffixIcon"
    :inputStyle="inputStyle"
    @focus="onFocus"
    @blur="onBlur"
    @clear="onClear"
    @change="onChange" />
    
</template>

<style scoped lang='scss'>

</style>

以下の下にplugins/components.tsグローバルに登録します。

import type {
    
     Component } from 'vue'
import ArInput from '@/components/form/input/index.vue';

const componentObj: {
    
    [propName: string]: Component} = {
    
    
  ArInput
};

export default function install(app: any) {
    
    
  Object.keys(componentObj).forEach((key) => {
    
    
    app.component(key, componentObj[key])
  })
}

忘れずplugins/index.tsに追加してください:

import installComponents from './components'

export default (app: any) => {
    
     
  // ...

  // 注册自定义组件
  installComponents(app);
}

login.vueソースコード:

<script lang="ts" setup>
import type {
      
       FormInstance, FormRules } from 'element-plus'
import {
      
       ref, reactive } from 'vue'


const formRef = ref<FormInstance>();
const form = reactive({
      
      
  name: '',
  password: ''
})
const rules = ref<FormRules>({
      
      
  name: [
    {
      
       required: true, message: '请输入账号', trigger: 'blur' },
    {
      
       min: 8, max: 12, message: '账号长度为8-12', trigger: 'blur' },
  ],
  password: [
  {
      
       required: true, message: '请输入密码', trigger: 'blur' },
  ]
})

const onSubmit = async() => {
      
      
  await formRef.value.validate((valid, fields) => {
      
      
    if (valid) {
      
      
      console.log('submit!')
    } else {
      
      
      console.log('error submit!', fields)
    }
  })
}

</script>

<template>
  <div class="login">
    <div class="login-inner">
      <el-form ref="formRef" :model="form" :rules="rules">
        <el-form-item label="账号" prop="name" required>
          <ArInput v-model:value="form.name" prefix-icon="User" />
        </el-form-item>
        <el-form-item label="密码" prop="password" required>
          <ArInput v-model:value="form.password" type="password" prefix-icon="Lock" />
        </el-form-item>
        <el-button type="primary" class="login-btn" @click="onSubmit">登录</el-button>
      </el-form>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.login {
      
      
  width: 100vw;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  background-color: #ccc;

  &-inner{
      
      
    margin-top: 20%;
    width: 300px;
    padding: 32px;
    background-color: #fff;
    border-radius: 4px;
  }

  &-btn {
      
      
    width: 100%;
  }
}
</style>

最終ページの効果は次のとおりです。

ここに画像の説明を挿入

環境変数

ページ送信アクションを完了する前に、まず環境変数の問題を解決します。テスト サービス、プレリリースまたは実稼働環境では常に異なる変数がいくつかあるため、環境を区別する必要があります。

新しいフォルダーを作成しenv、その中に 3 つまたは 4 つのファイルを追加します。

.env                # 所有情况下都会加载
.env.development     # 开发环境
.env.release         # 预发布环境
.env.production      # 正服环境

例えば:

# .env.development 
VITE_ENV = devalopment
# 请求接口
VITE_API_URL = https://api.vvhan.com/testapi/saorao

このようにして、変数をさまざまな環境に設定でき、環境を区別するためにスクリプト コマンドを変更します。

"scripts": {
    
    
    "watch": "vite",
    "watch:release": "vite --mode release",
    "watch:production": "vite --mode production",
    "build:development": "vue-tsc && vite build --mode development",
    "build:release": "vue-tsc && vite build --mode release",
    "build:production": "vue-tsc && vite build --mode production",
    // ...
  },

ルート ディレクトリに新しいファイルを作成しますbuild/utils.ts

// Read all environment variable configuration files to process.env
export function wrapperEnv(envConf: Recordable) {
    
    
  const result: any = {
    
    }

  for (const envName of Object.keys(envConf)) {
    
    
    let realName = envConf[envName].replace(/\\n/g, '\n')
    realName =
      realName === 'true' ? true : realName === 'false' ? false : realName

    result[envName] = realName
    if (typeof realName === 'string') {
    
    
      process.env[envName] = realName
    } else if (typeof realName === 'object') {
    
    
      process.env[envName] = JSON.stringify(realName)
    }
  }
  return result
}

次に、vite.config.js定義した変数を変更して挿入します。

import {
    
     defineConfig, loadEnv, UserConfig, ConfigEnv  } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {
    
     ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import {
    
     wrapperEnv } from './build/utils'

const resolve = (dist) => path.resolve(__dirname, dist)

// https://vitejs.dev/config/
export default ({
    
     command, mode }: ConfigEnv): UserConfig => {
    
    
  const env = loadEnv(mode, './env')
  wrapperEnv(env)

  return {
    
    
    plugins: [
      vue(),
      AutoImport({
    
    
        imports: [
          'vue',
          'vue-router',
          'pinia'
        ],
        eslintrc: {
    
    
          enabled: false,
          filepath: './.eslintrc-auto-import.json',
          globalsPropValue: true
        },
        resolvers: [ElementPlusResolver()]
      }),
      Components({
    
    
        resolvers: [ElementPlusResolver()]
      })
    ],
    resolve: {
    
    
      alias: {
    
    
        '@': resolve('./src')
      },
      extensions: [".js", ".ts", ".tsx", ".jsx"]
    },
    css: {
    
    
      preprocessorOptions: {
    
    
        scss: {
    
    
          additionalData: '@import "@/assets/styles/variable.scss";@import "@/assets/styles/mixins.scss";'
        }
      }
    }
  }
}

このようにして、axios以下のパッケージで使用できます。そうそう、TS エラーが少しあるかもしれません。変更してくださいtsconfig.node.json:

"include": ["vite.config.ts", "build**/*.ts"]

アクシオスパッケージ

新しいsrc/utils/request.ts:

import axios, {
    
     AxiosInstance, InternalAxiosRequestConfig } from 'axios'
import {
    
     ElMessage } from 'element-plus'

const request: AxiosInstance | any = axios.create({
    
    
  timeout: 100000,
  headers: {
    
    
    post: {
    
    
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  },
  withCredentials: true
});

request.interceptors.request.use((config: InternalAxiosRequestConfig) => {
    
    
  let token = localStorage.getItem("token");
  if (token && token !== '') {
    
    
    config.headers['Authorization'] = token;
  }

  // 获取环境变量!!!
  const projectUrlPrefix = import.meta.env.VITE_API_URL;
  // 这样更支持多域名接口的情况
  if (config && config.url && !/^(http(|s):\/\/)|^\/\//.test(config.url)) {
    
    
    config.url = projectUrlPrefix + config.url;
  }
  return config;
});

request.interceptors.response.use((res: any) => {
    
    
  if (res.data.status && res.data.status !== 200) {
    
    
    ElMessage.error(res.data.msg || '请求失败,请稍后重试')
    return Promise.reject(res.data)
  }
  // 如果这里是登录信息过期,那么应该给个弹窗提示什么的,最后都应该重定向到登录页面
  return res.data
}, (error: any) => {
    
    
  console.log(`%c 接口异常 `, 'background-color:orange;color: #FFF;border-radius: 4px;', error);
})

export default request;
export const $get = (url: string, params = {
    
    }) => {
    
    
  return request.get(url, {
    
    
    params
  })
}
export const $post = (url: string, params = {
    
    }) => {
    
    
  return request.post(url, params)
}

ログイン ページに移動してテストを実行します。

<script lang="ts" setup>
const $post = inject('$post') as any
// ...

const onSubmit = async () => {
  // ...
  const res = await $post('/login', form)
  // ...
}
</script >

異なる環境でドメイン名のプレフィックスをテストします。異なる場合、構成は成功しています~

https://api.vvhan.com/testapi/saorao/login
https://api.vvhan.com/releaseapi/saorao/login
https://api.vvhan.com/api/saorao/login

全体的な配置

ここに画像の説明を挿入

  • ルーティングモジュール内のパスを左側に表示されるメニューに変換します。
  • 上の部分はユーザー情報であり、終了できます。
  • ルートを切り替えると、ブラウザに似たタブが表示されます。タブをクリックして切り替えるか、現在のページを閉じることができます。
  • 最後に、ページのメインコンテンツが表示されます。

まずメニュー コンポーネントを見てください。

<script lang='ts' setup>
import {
    
     ref, computed, watch } from 'vue';
import {
    
     useRoute, useRouter } from 'vue-router'
import {
    
     storeToRefs } from 'pinia'
import routes from '@/router/index';
import {
    
     IRouterItem } from '@/types/menu'
import {
    
     useTagViewsStore } from '@/store/tagViews';

const route = useRoute();
const router = useRouter();
const isCollapse = ref(false);
const defaultActive = ref('0')
const tagViewsStore = useTagViewsStore();
const {
    
     visitedViews } = storeToRefs(tagViewsStore);

// 1. 从声明的路由中获取当前显示的菜单栏(还可以过滤一些不是菜单栏的页面)
const currentMenu = computed(() => {
    
    
  if(routes && routes.length) {
    
    
    const routesArr: any = routes.filter((route) => route.name === 'Layout');
    if(routesArr && routesArr[0] && routesArr[0].children){
    
    
      const res = routesArr[0].children as IRouterItem[];
      return res;
    }else {
    
    
      return [];
    }
  }else {
    
    
    return []
  }
});

// 获取路由对应的菜单下标(如果一打开是客户列表页,则高亮客户列表)
const currentMenuToObj = computed(() => {
    
    
  const routes = currentMenu.value;
  if(routes && routes.length) {
    
    
    let obj: {
    
    [key: string]: any} = {
    
    };
    for(let i = 0; i < routes.length; i++) {
    
    
      const item = routes[i];
      if(item.children) {
    
    
        for(let j = 0; j< item.children.length; j++) {
    
    
          const subItem = item.children[j];
          obj[subItem.path] = {
    
    
            index: `${
      
      i}-${
      
      j}`,
            item: subItem
          };
        }
      }else {
    
    
        obj[item.path] = {
    
    
          index: '' + i,
          item
        }; 
      }
    }

    return obj;
  }else {
    
    
    return {
    
    };
  }
})

// 监听路由获取当前高亮的值,store的使用在后面详细说一下
watch(
  () => route.path,
  (val: string) => {
    
    
    if(!visitedViews.value.length) {
    
    
      const item = {
    
    
        path: '/',
        name: 'Index',
        meta: {
    
     title: '主页' },
      }
      tagViewsStore.addVisitedViews(item)
      tagViewsStore.setActivitedView(item)
    }
    if(val) {
    
    
      const obj = currentMenuToObj.value[val];
      defaultActive.value = obj.index;
      tagViewsStore.addVisitedViews(obj.item)
      tagViewsStore.setActivitedView(obj.item)
    }
  }, {
    
    
    immediate: true
  }
)

// 点击菜单栏进行跳转
const onToPage = (item: IRouterItem) => {
    
    
  if(route.path === item.path) return;
  tagViewsStore.addVisitedViews(item)
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <div class="menu">
    <div class="menu-logo">Logo</div>
    <div class="menu-main">
      <el-menu
        :default-active="defaultActive"
        :collapse="isCollapse"
        background-color="#191919"
        text-color="#7e7e7e"
        active-text-color="#ffffff"
      >
        <template v-for="(item, index) in currentMenu" :key="index">
          <el-menu-item :index="'' + index" v-if="!item.children || !item.children.length" @click="onToPage(item)">
            <template #title>{
    
    {
    
     item.meta.title  }}</template>
          </el-menu-item>
          <el-sub-menu :index="'' + index" v-else>
            <template #title>
              <span>{
    
    {
    
     item.meta.title  }}</span>
            </template>
            <el-menu-item v-for="(subItem, subIndex) in item.children" :key="`${index}-${subIndex}`" :index="`${index}-${subIndex}`" @click="onToPage(subItem)">{
    
    {
    
     subItem.meta.title  }}</el-menu-item>
          </el-sub-menu>
        </template>
      </el-menu>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.menu {
    
    
  height: 100vh;
  max-width: 280px;
  background-color: #191919;
  
  &-logo {
    
    
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
  }

  &-main {
    
    
    height: calc(100vh - 100px);
  }
}
</style>

<style lang="scss">
.menu .el-menu {
    
    
  border: none !important;
}
</style>

次に上部のバーの情報です。

<script lang='ts' setup>

</script>

<template>
  <div class="nav">
    <div class="nav-left"></div>
    <div class="nav-right">
      <div class="nav-right-item">
        <el-dropdown>
          <span class="el-dropdown-link">
            您好,XXX
          </span>
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item>退出登录</el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </div>
    </div>
  </div>
</template>

<style scoped lang='scss'>
.nav {
      
      
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 16px;
  height: 60px;
  border-bottom: solid 1px var(--el-menu-border-color);
  background-color: #fff;

  &-right{
      
      
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: flex-end;

    &-item {
      
      
      cursor: pointer;
    }
  }
}
</style>

次にナビゲーションバーの開発ですがtab、ここでは状態管理にpiniaを使用します。インストール後、まず以下plugins/index.tsに登録します。

import {
    
     createPinia } from 'pinia';

// ...
// 注册store (建议这个放在所有注册的首位,方便其他插件可能会用到它)
app.use(createPinia());

ナビゲーションに関するステートメントtabstore次のとおりですsrc/store/tagViews.ts

import {
    
     defineStore } from 'pinia';
import {
    
     IRouterItem } from '@/types/menu'

export const useTagViewsStore = defineStore('tagViews', {
    
    
  state: () => {
    
    
    return {
    
    
      // 访问过的页面
      visitedViews: [] as IRouterItem[],
      // 当前访问的页面
      activitedView: {
    
    } as IRouterItem
    }
  },

  actions: {
    
    
    // 新增页面
    addVisitedViews(view: IRouterItem){
    
    
      const item = this.visitedViews.find((item) => item.path === view.path)
      if(item) return;
      this.visitedViews.push(view);
    },
    
    // 删除页面
    deleteVisitedViews(index: number) {
    
    
      this.visitedViews.splice(index, 1);
    },
    
    // 高亮某个页面
    setActivitedView(view: IRouterItem) {
    
    
      this.activitedView = view
    }
  }
})

tagViewsコンポーネントのソースコードは次のとおりです。

<script lang='ts' setup>
import {
      
       storeToRefs } from 'pinia';
import {
      
       useRouter } from 'vue-router'
import {
      
       useTagViewsStore } from '@/store/tagViews';
import {
      
       IRouterItem } from '@/types/menu'

const router = useRouter();
const tagViewsStore = useTagViewsStore();
const {
      
       visitedViews, activitedView } = storeToRefs(tagViewsStore);

// 关闭页面
const onDel = (item: IRouterItem) => {
      
      
  const index = visitedViews.value.findIndex((view) => view.path === item.path);
  if(index === -1) return;

  tagViewsStore.deleteVisitedViews(index);
  if(item.path === activitedView.value.path) {
      
      
    const obj = visitedViews.value[index - 1]
    tagViewsStore.setActivitedView(obj);
    router.push(obj.path)
  }
}

// 切换页面
const onChange = (item: IRouterItem) => {
      
      
  tagViewsStore.setActivitedView(item)
  router.push(item.path)
}

</script>

<template>
  <el-scrollbar class="tags-scrollbar">
    <div class="tags">
      <div v-for="item in visitedViews" :key="item.path" :class="['tags-item', { active: activitedView.path === item.path }]" @click="onChange(item)" >
        <span class="tags-item-title">{
   
   { item.meta ? item.meta.title : '' }}</span>
        <el-icon v-if="item.path !== '/'" @click.stop="onDel(item)"><Close /></el-icon>
      </div>
    </div>
  </el-scrollbar>
</template>

<style scoped lang='scss'>
.tags-scrollbar {
      
      
  height: 30px;
  overflow: hidden;
}
.tags {
      
      
  display: flex;
  background: #f3f3f3;
  border: 1px solid #f2f2f2;
  border-right: none;
  margin: -1px 0 0 -1px;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  &-item {
      
      
    display: flex;
    align-items: center;
    position: relative;
    cursor: pointer;
    height: 26px;
    line-height: 26px;
    border: 1px solid #d8dce5;
    color: #495060;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: 5px;
    margin-top: 4px;

    &-title {
      
      
      margin-right: 4px;
    }

    &:first-of-type {
      
      
      margin-left: 5px;
    }
    &:last-of-type {
      
      
      margin-right: 5px;
    }
    &.active {
      
      
      background-color: #e25050;
      color: #fff;
      border-color: #e25050;
      &::before {
      
      
        content: '';
        background: #fff;
        display: inline-block;
        width: 8px;
        height: 8px;
        border-radius: 50%;
        position: relative;
        margin-right: 2px;
      }
    }
  }
}
</style>

piniaの使用率はvuex非常に単純ですが、最大の違いは * mutations* が存在しなくなったことです。

ルートインターセプト

通常、ログイン後、piniaWeb サイト上のユーザーに関する情報が更新され、一部の情報は暗号化されて Web サイトに保存されますlocalstorageログインしていないユーザーの場合は、システムへのアクセスを傍受し、ログイン ページにリダイレクトする必要があります。(実際のプロジェクトではページ権限の傍受も考慮する必要があります)

// router/hooks/index.ts
import type {
    
     Router } from 'vue-router'
import {
    
     USERINFO } from '@/constants/localstorage'

const routerHook = (router: Router) => {
    
    
  router.beforeEach(to => {
    
    
    if(to.path === '/login') {
    
    
      // 可以做一些清空登录信息的操作, 比如跟pinia相关的等操作
      localStorage.removeItem(USERINFO);
      return true;
    }else{
    
    
      // 在这里可以判断用户是否登录,跳转的某个页面是否有权限,这里只是粗略写一下
      const info = localStorage.getItem(USERINFO);
      if(!info) {
    
    
        return {
    
     name: 'Login' }
      }
    } 
  })
}

export default routerHook;


import {
    
     createRouter, createWebHashHistory } from 'vue-router'
import routes from './routes'
import routerHook from './hooks/index'

// 注册路由
const router = createRouter({
    
    
  history: createWebHashHistory(),
  routes
})

routerHook(router)

export default router

ログイン ページを変更してシミュレートします。

const onSubmit = async () => {
    
    
  await formRef.value.validate(async (valid: boolean, fields: {
     
     [key: string]: any}) => {
    
    
    if (valid) {
    
    
      // 一般这种情况下,localStorage中存储的信息不能太重要,且需要加密,还应该更新pinia中的用户信息
      const info = {
    
    
        name: 'Armouy'
      }
      localStorage.setItem(USERINFO, JSON.stringify(info))
      router.push('/')
    } else {
    
    
      console.log('error submit!', fields)
    }
  })
}

ログインしていない場合は、ログインページにリダイレクトされます。

パック

npm run build:production
npm run preview

参考リンク


間違いがあればご指摘ください、読んでいただきありがとうございます~

おすすめ

転載: blog.csdn.net/qq_34086980/article/details/131371511