Write the Code. Change the World.

11月 30

如果既想用 bootstrap 框架,又想用 vue 的语法。那 bootstrap-vue 就是个不错的选择。

官网:https://bootstrap-vue.org/

怎么做呢

  • 第一步,用脚手架先建立一个空的 vue 项目。
vue create btvue.cn

# 如果提示 bash: vue: command not found,那就是 vue cli 没有装。装一下。
# https://cli.vuejs.org/zh/guide/installation.html
npm install -g @vue/cli

# 然后再 vue create btvue.cn 
# 就这样创建了一个基础的 vue 项目

  • 第二步,安装 bootstrap-vue bootstrap axios
npm install bootstrap-vue bootstrap axios
  • 第三步,设置 bootstrap vue

    在 src/main.js 中引入 bootstrap-vue,并注册它。

# src/main.js

import BootstrapVue from 'bootstrap-vue'
Vue.use(BootstrapVue)

除此,还需要导入样式文件。

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

仅仅上边这样是不够的。上边只是引入了 bootstrap vue。而一个完整的项目,是离不开路由,请求,状态管理这些的。

配置 vue.config.js 以及 settings

使用 vue cli 初始化创建的项目,目录里是没有 vue.config.js 文件的,我们需要手动加入。

在根目录下创建以下三个文件。
- src/settings.js
- .env.development
- .env.production
- vue.config.js

填充下边的内容。

src/settings.js ,.env.development ,.env.production 这两个文件是编译时,某些程序内的配置。比如接口的调用地址等。

# src/settings.js src 目录下
module.exports = {

  title: '美好区块链'
}

# .env.development
# just a flag
ENV = 'development'

# base api
VUE_APP_BASE_API = '/dev-api'

# .env.production
# just a flag
ENV = 'production'

# base api
VUE_APP_BASE_API = '/prod-api'

vue.config.js 文件配置如下。

'use strict'
const path = require('path')
const defaultSettings = require('./src/settings.js')

function resolve(dir) {
  return path.join(__dirname, dir)
}

const name = defaultSettings.title || 'Vue' // page title

// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following methods:
// port = 9528 npm run dev OR npm run dev --port = 9528
const port = process.env.port || process.env.npm_config_port || 9528 // dev port

// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
  /**
   * You will need to set publicPath if you plan to deploy your site under a sub path,
   * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
   * then publicPath should be set to "/bar/".
   * In most cases please use '/' !!!
   * Detail: https://cli.vuejs.org/config/#publicpath
   */
  publicPath: '/',
  outputDir: 'dist',
  assetsDir: 'static',
  lintOnSave: process.env.NODE_ENV === 'development',
  productionSourceMap: false,
  devServer: {
    port: port,
    open: true,
    overlay: {
      warnings: false,
      errors: true
    },
    before: ''
  },
  configureWebpack: {
    // provide the app's title in webpack's name field, so that
    // it can be accessed in index.html to inject the correct title.
    name: name,
    resolve: {
      alias: {
        '@': resolve('src')
      }
    }
  },
  chainWebpack(config) {
    // it can improve the speed of the first screen, it is recommended to turn on preload
    config.plugin('preload').tap(() => [
      {
        rel: 'preload',
        // to ignore runtime.js
        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
        fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
        include: 'initial'
      }
    ])

    // when there are many pages, it will cause too many meaningless requests
    config.plugins.delete('prefetch')

    // set svg-sprite-loader
    config.module
      .rule('svg')
      .exclude.add(resolve('src/icons'))
      .end()
    config.module
      .rule('icons')
      .test(/\.svg$/)
      .include.add(resolve('src/icons'))
      .end()
      .use('svg-sprite-loader')
      .loader('svg-sprite-loader')
      .options({
        symbolId: 'icon-[name]'
      })
      .end()

    config
      .when(process.env.NODE_ENV !== 'development',
        config => {
          config
            .plugin('ScriptExtHtmlWebpackPlugin')
            .after('html')
            .use('script-ext-html-webpack-plugin', [{
            // `runtime` must same as runtimeChunk name. default is `runtime`
              inline: /runtime\..*\.js$/
            }])
            .end()
          config
            .optimization.splitChunks({
              chunks: 'all',
              cacheGroups: {
                libs: {
                  name: 'chunk-libs',
                  test: /[\\/]node_modules[\\/]/,
                  priority: 10,
                  chunks: 'initial' // only package third parties that are initially dependent
                },
                commons: {
                  name: 'chunk-commons',
                  test: resolve('src/components'), // can customize your rules
                  minChunks: 3, //  minimum common number
                  priority: 5,
                  reuseExistingChunk: true
                }
              }
            })
          // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
          config.optimization.runtimeChunk('single')
        }
      )
  }
}

然后,修改 package.json 中的 scripts 部分,这里用来打包和运行的命令。

    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "build:dev": "vue-cli-service build --mode development",
    "lint": "vue-cli-service lint"

配置 vue-router 以及 vuex

下边的架构来源于: vue element admin

vue-router 和 vuex 是 vue 里的关键,下边要把这两个配置到项目中来。js-cookie, nprogress 也会用到。

先安装。

npm install vue-router

npm install vuex

npm install js-cookie

npm install nprogress

还要安装处理 scss 的 loader。

npm install sass-loader --save-dev

npm install node-sass --save-dev

# vue 全局使用 svg icon
npm install svg-sprite-loader --save-dev

初始的完整的数据架构如下:

├─ .env.development
├─ .env.production
├─ .gitignore
├─ babel.config.js
├─ package-lock.json
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
├─ README.md
├─ src
│ ├─ api
│ │ └─ user.js
│ ├─ App.vue
│ ├─ assets
│ │ └─ logo.png
│ ├─ components
│ │ └─ SvgIcon
│ │ └─ index.vue
│ ├─ icons
│ │ ├─ index.js
│ │ ├─ svg
│ │ │ └─ search.svg
│ │ └─ svgo.yml
│ ├─ main.js
│ ├─ permission.js
│ ├─ router
│ │ └─ index.js
│ ├─ settings.js
│ ├─ store
│ │ ├─ getters.js
│ │ ├─ index.js
│ │ └─ modules
│ │ └─ user.js
│ ├─ styles
│ │ └─ index.scss
│ ├─ utils
│ │ ├─ auth.js
│ │ ├─ get-page-title.js
│ │ ├─ request.js
│ │ └─ validate.js
│ └─ views
│ ├─ 404.vue
│ ├─ index.vue
│ └─ login
│ └─ index.vue
└─ vue.config.js

下边,我们针对 vue-router,vuex,api,utils 等文件一一进行配置处理。

code 一 一 展现

先填充 src/utils/ 下的工具类。

auth.js

import Cookies from 'js-cookie'

const TokenKey = 'vue_token'

export function getToken() {
  return Cookies.get(TokenKey)
}

export function setToken(token) {
  return Cookies.set(TokenKey, token)
}

export function removeToken() {
  return Cookies.remove(TokenKey)
}

get-page-title

import defaultSettings from '@/settings'

const title = defaultSettings.title || 'Vue'

export default function getPageTitle(pageTitle) {
  if (pageTitle) {
    return `${pageTitle} - ${title}`
  }
  return `${title}`
}

request.js

import axios from 'axios'
import store from '@/store'
import { getToken } from '@/utils/auth'

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['X-Token'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    return response.data
  },
  error => {
    console.log('err' + error) // for debug
    return Promise.reject(error)
  }
)

export default service

validate.js

/**
 * @param {string} path
 * @returns {Boolean}
 */
export function isExternal(path) {
  return /^(https?:|mailto:|tel:)/.test(path)
}

再来填充 vuex 相关的。

src/store/modules/user.js

import { login, logout, getInfo } from '@/api/user'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'

const getDefaultState = () => {
  return {
    token: getToken(),
    nickname: '',
    avatar: ''
  }
}

const state = getDefaultState()

const mutations = {
  RESET_STATE: (state) => {
    Object.assign(state, getDefaultState())
  },
  SET_TOKEN: (state, token) => {
    state.token = token
  },
  SET_USER: (state, data) => {
    const fillable = ['nickname', 'avatar']
    for(const key in data) {
      if (fillable.indexOf(key) >= 0) {
        state[key] = data[key]
      }
    }
  },
}

const actions = {
  // user login
  login({ commit }, userInfo) {
    const { account, password } = userInfo
    return new Promise((resolve, reject) => {
      login({ account: account.trim(), password: password }).then(response => {
        const { data } = response
        commit('SET_TOKEN', data.token)
        setToken(data.token)
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // get user info
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo(state.token).then(response => {
        const { data } = response

        if (!data) {
          return reject('请登录')
        }

        commit('SET_USER', data)
        resolve(data)
      }).catch(error => {
        reject(error)
      })
    })
  },

  // user logout
  logout({ commit, state }) {
    return new Promise((resolve, reject) => {
      logout(state.token).then(() => {
        removeToken() // must remove  token  first
        resetRouter()
        commit('RESET_STATE')
        resolve()
      }).catch(error => {
        reject(error)
      })
    })
  },

  // remove token
  resetToken({ commit }) {
    return new Promise(resolve => {
      removeToken() // must remove  token  first
      commit('RESET_STATE')
      resolve()
    })
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

src/store/getters.js

const getters = {
  token: state => state.user.token,
  avatar: state => state.user.avatar,
  nickname: state => state.user.nickname
}
export default getters

src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import user from './modules/user'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    user
  },
  getters
})

export default store

再来看看 src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export const constantRoutes = [
  {
    path: '/',
    component: () => import('@/views/index'),
  },
  {
    path: '/login',
    component: () => import('@/views/login/index'),
  },
  {
    path: '/404',
    component: () => import('@/views/404'),
  },
  // 404 page must be placed at the end !!!
  { path: '*', redirect: '/404'}
]

const createRouter = () => new Router({
//   mode: 'history', // require service support
  scrollBehavior: () => ({ y: 0 }),
  routes: constantRoutes
})

const router = createRouter()

// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
  const newRouter = createRouter()
  router.matcher = newRouter.matcher // reset router
}

export default router

再来看看第一个组件 src/components/SvgIcon/index.vue

<template>
  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>
import { isExternal } from '@/utils/validate'

export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
    isExternal() {
      return isExternal(this.iconClass)
    },
    iconName() {
      return `#icon-${this.iconClass}`
    },
    svgClass() {
      if (this.className) {
        return 'svg-icon ' + this.className
      } else {
        return 'svg-icon'
      }
    },
    styleExternalIcon() {
      return {
        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
      }
    }
  }
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}

.svg-external-icon {
  background-color: currentColor;
  mask-size: cover!important;
  display: inline-block;
}
</style>

再来增加一个 icon 文件。 src/icons/svg/search.svg

<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1606724520860" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3076" width="128" height="128"><path d="M933.602 831.47 721.175 617.262c-2.472-2.479-5.204-4.372-7.813-6.435 35.884-55.518 56.836-121.647 56.836-192.878 0-195.694-157.298-354.31-351.461-354.31-194.054 0-351.461 158.616-351.461 354.31 0 195.722 157.407 354.302 351.461 354.302 70.631 0 136.303-21.179 191.459-57.361 2.009 2.692 3.852 5.369 6.283 7.827l212.454 214.235c14.506 14.579 33.396 21.849 52.314 21.849 18.932 0 37.836-7.268 52.328-21.814C962.436 907.824 962.436 860.61 933.602 831.47M418.737 660.37c-132.562 0-240.468-108.767-240.468-242.421 0-133.634 107.907-242.428 240.468-242.428 132.561 0 240.467 108.794 240.467 242.428C659.204 551.602 551.298 660.37 418.737 660.37" p-id="3077"></path></svg>

src/icons/index.js

import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg component

// register globally
Vue.component('svg-icon', SvgIcon)

const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

src/icons/svgo.yml

# replace default config

# multipass: true
# full: true

plugins:

  # - name
  #
  # or:
  # - name: false
  # - name: true
  #
  # or:
  # - name:
  #     param1: 1
  #     param2: 2

- removeAttrs:
    attrs:
      - 'fill'
      - 'fill-rule'

再来增加一个全局样式文件 src/styles/index.scss

// 下拉条
body::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}

body::-webkit-scrollbar-button {
    display: none;
}

body::-webkit-scrollbar-track {
    background-color: black;
}

body::-webkit-scrollbar-track-piece {
    background: #ffffff;
}

body::-webkit-scrollbar-thumb {
    background-color: #8E8E8E;
    border-radius: 3px;
}

body::-webkit-scrollbar-thumb:hover {
    background-color: #3b3b3b;
}

body::-webkit-scrollbar-resizer {
    background-color: #ff6e00;
}

// textarea 滚动条
textarea::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}

textarea::-webkit-scrollbar-button {
  display: none;
}

textarea::-webkit-scrollbar-track {
  background-color: black;
}

textarea::-webkit-scrollbar-track-piece {
  background: #ffffff;
}

textarea::-webkit-scrollbar-thumb {
  background-color: #8E8E8E;
  border-radius: 3px;
}

textarea::-webkit-scrollbar-thumb:hover {
  background-color: #8E8E8E;
}

// 自定义颜色 nprogress 颜色
#nprogress .bar {
  background: #2889fc !important;
}

再增加 view 文件。

src/pages/login/index.vue

<template>
    <div>
        login
    </div>
</template>

<script>
export default {
    data() {
        return {

        }
    }
}
</script>

<style scoped>

</style>

src/pages/404.vue

<template>
    <div>
        404
    </div>
</template>

<script>
export default {
    data() {
        return {

        }
    }
}
</script>

<style scoped>

</style>

src/pages/index.vue

<template>
    <div class="container">
        home
    </div>
</template>

<script>
export default {
    data() {
        return {

        }
    }
}
</script>

<style scoped>

</style>

再来修改 src/App.vue

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {};
  },
};
</script>

再来修改 src/main.js

import Vue from 'vue'
import App from './App.vue'
import BootstrapVue from 'bootstrap-vue'
import store from './store'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'
import '@/styles/index.scss' // global css
import '@/icons' // icon
import '@/permission' // permission control


Vue.use(BootstrapVue)

Vue.config.productionTip = false

new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
})

再来一个 src/permission.js

import router from './router'
import store from './store'
import NProgress from 'nprogress' // progress bar
import getPageTitle from '@/utils/get-page-title'
import { getToken } from '@/utils/auth' // get token from cookie
import 'nprogress/nprogress.css' // progress bar style

NProgress.configure({ showSpinner: false }) // NProgress Configuration

router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()

  document.title = getPageTitle(to.meta.title)

  next()
})

NProgress.configure({ showSpinner: false }) // NProgress Configuration

router.beforeEach(async (to, from, next) => {
  // start progress bar
  NProgress.start()

  // set page title
  document.title = getPageTitle(to.meta.title)

  // determine whether the user has logged in
  const hasToken = getToken()
  if (hasToken) {
    const hasGetUserInfo = store.getters.nickname
    if (!hasGetUserInfo) {
      try {
        await store.dispatch('user/getInfo')
      } catch (error) {
        await store.dispatch('user/resetToken')
      }
    }
  }
  next()
  NProgress.done()
})

router.afterEach(() => {
  // finish progress bar
  NProgress.done()
})

最后修改 src/settings.js

module.exports = {

  title: '美好开始'
}

到此,基础的 vue,vuex, vue-router, axios,icons 构建已经完成。到这里也只是一个基础。后边还需要继续去完善下去。

继续构造

对于一个网站,大多页面头部和尾部都是一样的。这种情况下,我们可以通过 layout 的方式来把这共同的部分抽象出来。

那就这么做。

新建 src/layout/components/Header/index.vue 和 src/layout/components/Footer/index.vue 。 填充下边的代码。

# src/layout/components/Header/index.vue
<template>
  <header class="header container-fluid">header</header>
</template>

# src/layout/components/Footer/index.vue
<template>
  <footer class="footer container-fluid">footer</footer>
</template>

然后修改 src/App.vue,在这里边引入使用。

<template>
  <div id="app">
    <headers />

    <router-view />

    <footers />
  </div>
</template>

<script>
import Headers from '@/layout/components/Header'
import Footers from '@/layout/components/Footer'

export default {
  name: "App",
  components: {
    Headers,
    Footers
  },
  data() {
    return {};
  },
};
</script>

看这里好奇怪,Header 名字使用 Headers, Footer 也使用 Footers。如果不这样弄。vue 识别不出来。

参考答案

https://segmentfault.com/a/1190000019085935

https://www.cnblogs.com/duanzhenzhen/p/13045613.html

https://www.php.cn/manual/view/34100.html

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注