如果既想用 bootstrap 框架,又想用 vue 的语法。那 bootstrap-vue 就是个不错的选择。
怎么做呢
- 第一步,用脚手架先建立一个空的 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