• ⚡ vue3 全家桶体验


    前置

    从创建一个简单浏览器导航首页项目展开,该篇随笔包含以下内容的简单上手

    • vite
    • vue3
    • vuex4
    • vue-router next

    预览效果有助于理清这些内容,限于篇幅,不容易展开叙述。由于项目逻辑简单,只使用了少量 API,我只是写这个小项目过把手瘾,所以对应标题 上手。如果您只是想学习 vue 周边的 API,那么,这篇文章将给您带来有限的知识。

    初始化项目

    使用 vite 初始化 vue3 项目。什么是 vite?Vite 是一个 Web 构建工具。开发过程中通过浏览器 ES Module 导入为您的代码提供服务,生成环境与 Rollup 捆绑在一起进行打包。

    特性:

    • 闪电般快速的冷服务器启
    • 动即时热模块更换(HMR)
    • 真正的按需编译

    vite 截至今天支持的功能:

    • Bare Module Resolving
    • Hot Module Replacement
    • TypeScript
    • CSS / JSON Importing
    • Asset URL Handling
    • PostCSS
    • CSS Modules
    • CSS Pre-processors
    • JSX
    • Web Assembly
    • Inline Web Workers
    • Custom Blocks
    • Config File
    • HTTPS/2
    • Dev Server Proxy
    • Production Build
    • Modes and Environment Variables
    npm init vite-app aweshome
    npm install
    npm run dev
    npm run build
    

    最终生成的目录结构与使用 vue-cli 相似:

    │  .npmignore
    │  a.txt
    │  index.html
    │  package.json
    ├─public
    │      favicon.ico
    └─src
        │  App.vue
        │  index.css
        │  main.js
        ├─assets
        │      logo.png
        └─components
                HelloWorld.vue
    

    可以在项目根目录下创建 vite.config.js 配置 Vite:

    module.exports = {
      // 导入别名
      // 这些条目可以是精确的请求->请求映射*(精确,无通配符语法)
      // 也可以是请求路径-> fs目录映射。 *使用目录映射时
      // 键**必须以斜杠开头和结尾**
      alias: {
        // 'react': '@pika/react',
        // 'react-dom': '@pika/react-dom'
        // '/@foo/': path.resolve(__dirname, 'some-special-dir'),
      },
      // 配置Dep优化行为
      optimizeDeps: {
        // exclude: ['dep-a', 'dep-b'],
      },
      // 转换Vue自定义块的功能。
      vueCustomBlockTransforms: {
        // i18n: src => `export default Comp => { ... }`,
      },
      // 为开发服务器配置自定义代理规则。
      proxy: {
        // proxy: {
        //   '/foo': 'http://localhost:4567/foo',
        //   '/api': {
        //     target: 'http://jsonplaceholder.typicode.com',
        //     changeOrigin: true,
        //     rewrite: path => path.replace(/^/api/, ''),
        //   },
        // },
      },
      // ...
    }
    

    更多配置可以参考Github

    另外,现在可以使用 vitepress 代替原来的 vuepress 构建文档或博客。

    vue-router next

    npm i vue-router@next
    

    src/router/index.js

    import {createRouter, createWebHistory} from 'vue-router'
    import Home from '../components/home/Home.vue'
    import Cards from '../components/cards/Cards.vue'
    
    const router = createRouter({
      history: createWebHistory(),
      routes: [
        // route -> routes
        {
          path: '/',
          name: 'home',
          component: Home,
        },
        {
          path: '/cards',
          name: 'cards',
          component: Cards,
        },
      ],
    })
    
    export default router
    

    vue router next 还添加了动态路由,解决规则冲突的问题。做过权限管理应该深有体会。更多配置可以参考 Github

    vuex4

    使用与 vuex3 相同的 API。

    安装

    npm i vuex@next
    

    src/constants 下存放了静态数据,它们都是如下形式:

    export const vue = [
      {
        title: 'vue',
        desc: 'Vue 是用于构建用户界面的渐进式的框架',
        link: 'https://cn.vuejs.org/v2/guide/',
        img: import('../assets/images/vue.png'), // require -> import
      },
      {
        title: 'vue Router',
        desc: 'Vue Router 是 Vue.js 官方的路由管理器。',
        link: 'https://router.vuejs.org/zh/',
        img: import('../assets/images/vue.png'),
      },
      // ...
    ]
    

    src/store/index.js

    import {createStore} from 'vuex'
    
    import {vue, react, wechat, across, compileBuild} from '../constants/docs'
    import {frontEndTools, OfficeTools} from '../constants/tools'
    import {tools, docs, community} from '../constants/asideData'
    import {blogs} from '../constants/community'
    
    const store = createStore({
      state: {
        asideData: [],
        mainData: [],
      },
      mutations: {
        setAsideData(state, key) {
          const asideActions = {
            '2': tools,
            '3': docs,
            '4': community,
          }
          state.asideData = asideActions[key]
        },
        setMainData(state, menuItemText) {
          const actions = new Map([
            ['前端工具', frontEndTools],
            ['办公工具', OfficeTools],
            ['vue', vue],
            ['react', react],
            ['微信开发', wechat],
            ['跨端框架', across],
            ['编译构建', compileBuild],
            ['博客', blogs],
          ])
          state.mainData = actions.get(menuItemText)
        },
      },
      actions: {},
      modules: {},
    })
    
    export default store
    

    main.js

    结合上文的 vuex vue-router 可以看出,vue3 核心插件的 api 都做了简化。

    import './index.css'
    import {createApp} from 'vue'
    import store from './store'
    import App from './App.vue'
    import router from './router'
    
    const app = createApp(App)
    
    app.use(store)
    app.use(router)
    app.mount('#app')
    

    sass

    npm i sass
    

    package.json > dependencies

    {
      "dependencies": {
        "vue": "^3.0.0-beta.15",
        "vue-router": "^4.0.0-alpha.13",
        "vuex": "^4.0.0-beta.2"
      },
      "devDependencies": {
        "@vue/compiler-sfc": "^3.0.0-beta.15",
        "sass": "^1.26.8",
        "vite": "^1.0.0-beta.1"
      }
    }
    

    components

    这个小项目本质上可以只有一个页面 .vue 构成,我将它拆分,便于阅读。

    App.vue
    <template>
      <Header />
      <main>
        <router-view></router-view>
      </main>
      <Footer />
    </template>
    
    <script>
    import Header from './components/Header.vue'
    import Footer from './components/Footer.vue'
    
    export default {
      name: 'app',
      components: {
        Header,
        Footer,
      },
    }
    </script>
    
    <style>
    main {
      flex: 1;
    }
    </style>
    
    components/cards/Aside.vue
    <template>
      <aside>
        <ul>
          <li :index="item.index" v-for="item in this.$store.state.asideData" :key="item.index" ref="menuItem" @click="handleSelect(item.value)">
            <i class="fas fa-home"></i>
            <span>{{ item.value }}</span>
          </li>
        </ul>
      </aside>
    </template>
    
    <script>
    import store from '../../store'
    
    export default {
      setup(props, context) {
        return {
          handleSelect(value) {
            store.commit('setMainData', value)
          },
        }
      },
    }
    </script>
    
    <style lang="scss">
    aside {
      flex: 1;
      background-color: rgb(238, 238, 238);
      height: 100%;
      li {
        display: flex;
        align-items: center;
        height: 56px;
        line-height: 56px;
        font-size: 14px;
        color: #303133;
        padding: 0 1.4rem;
        list-style: none;
        cursor: pointer;
        transition: border-color 0.3s, background-color 0.3s, color 0.3s;
        white-space: nowrap;
        &:hover {
          background-color: rgb(224, 224, 224);
        }
      }
    }
    
    @media screen and (max- 768px) {
      aside {
        display: none;
        &.active {
          display: block;
        }
      }
    }
    </style>
    
    components/cards/Cards.vue
    <template>
      <div id="card-outer">
        <Aside />
        <section></section>
      </div>
    </template>
    
    <script>
    import Aside from './Aside.vue'
    import router from '../../router'
    
    export default {
      components: {
        Aside,
      },
    }
    </script>
    
    <style lang="scss">
    #card-outer {
      display: flex;
      align-content: stretch;
      height: 100%;
      & > section {
        flex: 8;
      }
    }
    
    .main-card {
      margin: 10px 0;
      cursor: pointer;
      .main-card-content {
        display: flex;
        align-items: center;
        img {
           30px;
          height: 30px;
          margin-right: 10px;
        }
        .main-card-content-info {
           90%;
          h3 {
            font-size: 14px;
          }
          p {
            font-size: 12px;
            color: #888ea2;
            white-space: nowrap;
            text-overflow: ellipsis;
            overflow: hidden;
             100%;
            line-height: 1.8;
          }
        }
        span {
          margin-left: 10px;
          text-decoration: none;
          &:nth-of-type(1) {
            font-size: 18px;
            font-weight: 700;
            color: #ffa502;
            white-space: nowrap;
          }
          &:nth-of-type(2) {
            font-size: 14px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }
        }
      }
    }
    </style>
    
    components/home/Home.vue
    <template>
      <section id="search">
        <div class="search-sources" style="margin-bottom: 10px;">
          <span size="mini" type="primary" v-for="(item, index) in source" @click="changeSource(item.name)" :key="index" :style="`background:${item.color};border-color:${item.color}`"
            >{{ item.name }}
          </span>
        </div>
        <div class="searchbox" :class="searchbarStyle.className">
          <input :placeholder="searchbarStyle.placeholder" v-model="searchValue" clearable v-on:keyup.enter="submit" />
          <button @click="submit" slot="append" icon="el-icon-search">
            <i class="fas fa-search"></i>
          </button>
        </div>
      </section>
    </template>
    
    <script>
    export default {
      data: () => ({
        baseUrl: 'https://www.baidu.com/s?ie=UTF-8&wd=',
        searchValue: '',
        searchbarStyle: {
          className: 'baidu',
          placeholder: '百度一下,你就知道',
        },
        source: [
          {
            name: '百度',
            color: '#2932E1',
          },
          {
            name: '搜狗',
            color: '#FF6F17',
          },
          {
            name: 'Bing',
            color: '#0c8484',
          },
          {
            name: 'Google',
            color: '#4285F4',
          },
          {
            name: 'NPM',
            color: '#EA4335',
          },
        ],
      }),
      methods: {  // 可以在 vue3 中使用 options API
        changeSource(name) {
          const actions = new Map([
            [
              '百度',
              () => {
                this.baseUrl = 'https://www.baidu.com/s?ie=UTF-8&wd='
                this.searchbarStyle = {
                  className: 'baidu',
                  placeholder: '百度一下,你就知道',
                }
              },
            ],
            [
              'Bing',
              () => {
                this.baseUrl = 'https://cn.bing.com/search?FORM=BESBTB&q='
                this.searchbarStyle = {
                  className: 'bing',
                  placeholder: '必应搜索',
                }
              },
            ],
            [
              '搜狗',
              () => {
                this.baseUrl = 'https://www.sogou.com/web?query='
                this.searchbarStyle = {
                  className: 'sougou',
                  placeholder: '搜狗搜索',
                }
              },
            ],
            [
              'Google',
              () => {
                this.baseUrl = 'https://www.google.com/search?q='
                this.searchbarStyle = {
                  className: 'google',
                  placeholder: 'Google Search',
                }
              },
            ],
            [
              'NPM',
              () => {
                this.baseUrl = 'https://www.npmjs.com/search?q='
                this.searchbarStyle = {
                  className: 'npm',
                  placeholder: 'Search Packages',
                }
              },
            ],
          ])
          actions.get(name)()
        },
        submit() {
          const url = this.baseUrl + this.searchValue
          window.open(url)
        },
      },
    }
    </script>
    
    <style lang="scss">
    #search {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-content: stretch;
      margin: 0 auto;
      height: 40vh;
       40%;
      & > div {
        display: flex;
      }
    }
    
    .search-sources {
      span {
        margin-right: 0.5rem;
        padding: 0.4rem 0.6rem;
        color: #fff;
        font-size: 14px;
        line-height: 14px;
        border-radius: 2px;
        &:hover {
          filter: contrast(80%);
          transition: 0.3s;
        }
      }
    }
    
    .searchbox {
      padding-left: 1rem;
      height: 2.6rem;
      border-radius: 6px;
      background-color: #fff;
      border: 1px #ccc solid;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    
      input {
        flex: 7;
        border: none;
        font-size: 1rem;
      }
    
      button {
        flex: 1;
        i {
          margin-right: 0;
        }
      }
    }
    
    $sources-color: (
      baidu: #2932e1,
      bing: #0c8484,
      sougou: #ff6f17,
      google: #4285f4,
      npm: #ea4335,
    );
    
    $source-list: baidu bing sougou google npm;
    
    @each $source in $source-list {
      .#{$source} {
        &:hover {
          border-color: map-get($sources-color, $source);
          box-shadow: 0 2px 4px map-get($sources-color, $source);
          transition: all 0.5s;
        }
        input {
          &:hover {
            border-color: map-get($sources-color, $source);
          }
        }
      }
    }
    
    @media screen and (max- 768px) {
      #search {
         90%;
      }
    }
    </style>
    
    components/Header.vue
    <template>
      <header>
        <ul class="nav">
          <li @click="handleSelect('home')">
            <i class="fas fa-home"></i>
            <span>首页</span>
          </li>
          <li @click="handleSelect('tools')">
            <i class="fas fa-tools"></i>
            <span>工具</span>
          </li>
          <li @click="handleSelect('docs')">
            <i class="fas fa-file-alt"></i>
            <span>文档</span>
          </li>
          <li @click="handleSelect('community')">
            <i class="fas fa-comment-dots"></i>
            <span>社区</span>
          </li>
        </ul>
        <MobileMenu />
      </header>
    </template>
    
    <script>
    import MobileMenu from './MobileMenu.vue'
    import store from '../store'
    import router from '../router'
    
    export default {
      components: {
        MobileMenu,
      },
    
      setup() {
        const handleSelect = item => {
          store.commit('setAsideData', item)
          if (item === 'home') {
            router.replace({name: 'home'})
          } else {
            const actions = {
              tools: ['setMainData', '前端工具'],
              docs: ['setMainData', 'vue'],
              community: ['setMainData', '博客'],
            }
            store.commit(actions[item][0], actions[item][1])
            router.replace({name: 'cards'})
          }
        }
    
        return {
          handleSelect,
        }
      },
    }
    </script>
    
    <style lang="scss">
    header {
      display: flex;
      height: 60px;
      align-content: stretch;
      padding: 0 9.5rem;
    }
    
    .nav {
      display: flex;
      align-items: center;
      align-content: stretch;
      li {
        padding: 0.5rem 0.75rem;
        &:hover {
          background-color: #f3f1f1;
          & span {
            color: #3273dc;
          }
        }
      }
    }
    
    @media screen and (max- 768px) {
      header {
        padding: 0;
      }
    }
    </style>
    
    components/MobileMenu.vue
    <template>
      <section id="mobile-menu">
        <div id="navbarBurger" class="navbar-burger burger" data-target="navMenuMore" :class="{active}" @click="sideToggle">
          <span></span>
          <span></span>
          <span></span>
        </div>
      </section>
    </template>
    
    <script>
    export default {
      data: () => ({
        active: false,
      }),
      methods: {
        sideToggle() {
          this.active = !this.active
          const classList = document.querySelectorAll('aside')[0].classList
          this.active ? classList.add('active') : classList.remove('active')
        },
      },
    }
    </script>
    
    <style lang="scss">
    #mobile-menu {
      display: none;
      position: absolute;
      right: 0;
      top: 0;
      z-index: 999999;
    }
    
    @media screen and (max- 768px) {
      #mobile-menu {
        display: block;
        .navbar-burger {
          position: relative;
          color: #835656;
          cursor: pointer;
          height: 60px;
           60px;
          margin-left: auto;
          span {
            background-color: #333;
            display: block;
            height: 1px;
            left: calc(50% - 8px);
            position: absolute;
            transform-origin: center;
            transition-duration: 86ms;
            transition-property: background-color, opacity, transform;
            transition-timing-function: ease-out;
             16px;
            &:nth-child(1) {
              top: calc(50% - 6px);
            }
            &:nth-child(2) {
              top: calc(50% - 1px);
            }
            &:nth-child(3) {
              top: calc(50% + 4px);
            }
          }
          &.active {
            span {
              &:nth-child(1) {
                transform: translateY(5px) rotate(45deg);
              }
              &:nth-child(2) {
                opacity: 0;
              }
              &:nth-child(3) {
                transform: translateY(-5px) rotate(-45deg);
              }
            }
          }
        }
      }
    }
    </style>
    

    最后

    一套流程下来,vite 给我的感觉就是“快”。对于 vue 周边, API 都是做了一些简化,如果你对 esm 有些了解,将更有利于组织项目,可读性相比 vue2.x 也更高。也有一些问题,限于篇幅,本文没有探讨。做项目还是上 vue2.x 及其周边。另外,我没找到 vue3 组件库。

  • 相关阅读:
    关于c#中的委托和事件
    Unity3d中默认函数调用顺序(MonoBehaviour)
    u3d 摄像机详解
    u3d中的坐标系
    u3d中的向量 vector3 vector2
    u3d中的INput
    C#构造函数
    解析C#中[],List,Array,ArrayList的区别及应用
    Mybatis(七) mybatis的逆向工程的配置详解
    Mybatis(六) Spring整合mybatis
  • 原文地址:https://www.cnblogs.com/guangzan/p/13194444.html
Copyright © 2020-2023  润新知