https://feiniao111.github.io/vueWebTpl-live/#/
git clone https://github.com/feiniao111/vueWebTpl.git
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
src
├─api ........................................................//http请求
│ └─examples
├─apiMock .............................................//http请求mock,与api一一对应
│ └─examples
├─assets ...................................................//存放样式文件、图片等资源
├─components ......................................//存放通用组件
├─examples ............................................//存放实例
│ ├─routerUsage
│ └─vuexUsage
├─lib ...........................................................//全局库目录
│ ├─directives .........................................//指令
│ ├─locale .................................................//国际化目录
│ │ └─lang ................................................//语言包
│ ├─mixins ................................................//混合
│ └─utils .....................................................//通用函数
├─pages .....................................................//页面组件,模块化存放
│ └─pageDemo1
├─router .....................................................//路由文件,模块化存放
└─store .......................................................//vuex文件,模块化存放
└─modules
├─common
└─pageDemo1
每个目录下都有一个README.md文件,说明该目录的用途。这些目录看起来多且复杂,但只要结合框架提供的demo来理解,很快就能掌握。
虽然有vuex这样的官方插件来帮助我们进行状态管理。但是,在很多情形下,我们还是需要设置一些全局变量(window),比如:
- 全局使用到的常量(token、http请求的超时时间等等)
- vuex无法覆盖的地方(非.vue的js、jq插件,和cef的C++端交互等)
但是全局变量如果管理不当,容易引发滥用,不易维护。
常见的全局变量管理方法有两种,一种是设定一个namespace,将变量挂载到该名空间下,另一种方法是采用闭包。
这里采用第二种做法,将全局变量作为私有变量'保护'起来,只暴露get和set方法。
// static/global.js
window.myGlobalClosure = (function () {
var __object = {
vueInst: undefined, // vue实例
i18nLanguage: 'chn',
}
return {
// 设置vue实例
setVueInst: function (instance) {
__object.vueInst = instance
},
getVueInst: function () {
return __object.vueInst
},
// 设置国际化语言
setLang: function (lang) {
if (__object.vueInst) {
__object.vueInst.$store.commit('common/SET_I18N_LANGUAGE', {
vue: __object.vueInst,
lang: lang
})
}
__object.i18nLanguage = lang
},
getLang: function () {
return __object.i18nLanguage
}
}
})()
全局变量文件在入口html处载入。
// index.html
<body>
<div id="app"></div>
<!-- 项目使用到的全局变量 -->
<script type="text/javascript" src="static/global.js"></script>
<!-- built files will be auto injected -->
</body>
这样,我们就可以在.vue或js插件中通过window.myGlobalClosure.setXX()
或者window.myGlobalClosure.getXX()
设置或访问全局变量。
本项目模板提供了丰富的例子,对常见的场景(http请求、vue-router、vuex、国际化等)用法进行介绍
这里采用官方推荐插件axios,并且给出了常用的get、post请求用法,多个请求如何并发执行、顺序执行,请求如何设置全局超时时间,添加token、断网处理等等。
具体戳这里
httpUsage
值得说明的是,我们将axios绑定到vue实例原型
import axios from 'axios'
Vue.prototype.$http = axios // 绑定到原型
这样就可以在.vue文件中通过
this.$http.get()
this.$http.post()
...
来请求资源。
另外,项目模板将http请求统一放置到api
目录中,
// api/examples/vuexUsage.js
import axios from 'axios'
const HTTP = axios
export default {
setNickname (name) {
return HTTP.post('/setNickname', {name: name})
}
}
然后在业务中调用http请求对应的封装方法,这样既可以减少业务中http请求的代码量,又能够实现http请求的复用。
http请求mock是开发中很重要的一环。当前端开发和后端开发人员约定好接口返回后,就可以并行进行开发,此时http请求的返回由mock数据暂时取代。另外,一些异常场景不容易触发,这时的返回数据也可以通过mock进行构造,从而验证此时的前端交互是否正常。
我们的项目模板采用mockjs来mock数据和拦截请求。
npm install mockjs --save-dev
同http请求一样,我们设置一个apiMock
目录来存放http请求mock,目录内文件也是按模块进行组织。http请求mock代码如下:
// src/apiMock/examples/httpUsage.js
/**
* demo api请求Mock
* mockjs官方文档:
* https://github.com/nuysoft/Mock
*/
var Mock = require('mockjs')
// 拦截url,返回模拟数据
Mock.mock('/postReq1', 'post', {
'data': {
'cur_page': '这是mock的数据'
}
})
// 在这里添加mock方法,拦截其他请求
Mock.mock('/login', 'post', {
status: 'success'
})
Mock.mock('/loginInfo', 'get', {
uid: 'xiaoming',
age: 18
})
...
当启用mock后,请求/postReq1
将被mockjs插件拦截,不会发送到服务器中。拦截返回的数据为
{
'data': {
'cur_page': '这是mock的数据'
}
}
灵活开启和关闭mock
我们使用mock的目的为了加快开发和调试的速度,同时尽可能少地污染原生http请求。
一种可行的方案是,增加一个专门的mock环境,类似dev环境,通过执行
npm run mock
启动。mock环境和dev环境非常类似,区别只是前者会加载mockjs
,对http请求进行拦截。
// build目录中增加一套mock配置,基本同dev
// 在main.js
process.env == 'mock' && require('@/apiMock/index.js')
// src/apiMock/index.js
// 按页面来引用mock文件
require('./examples/httpUsage') // 具体内容见上面
require('./examples/vuexUsage')
...
采用这种方案,完全不会污染开发环境和生产环境,即执行npm run dev
和npm run build
,都不会加载mockjs
,http请求都能正常地发送和返回。但是,这种方案有一个明显的缺点——mock环境和dev环境来回切换比较麻烦,如果项目比较大,切换时间会比较长。
我们的项目模板,采取一种更灵活的方式,可以快速的在dev环境中启用和关闭mock。该方案的思路是:在需要时加载mock文件,通过重载页面关闭mock
在需要时加载mock文件
由于mockjs
通过require()
方法开启,因此可以将require()
封装成函数,在想要mock的时候调用这个函数。我们将该方法暴露给全局闭包,以便在console中就可以调用。注意,由于全局闭包所在文件static/global.js
文件并没有被babel编译,不支持require()
,故需将mock启用封装在vuex的mutation中,通过闭包的vue实例调用。
// static/global.js
window.myGlobalClosure = (function () {
var __object = {
vueInst: undefined, // vue实例
isMockMode: false, // 是否mock模式,初始为关闭
}
return {
// 设置vue实例
setVueInst: function (instance) {
__object.vueInst = instance
},
getVueInst: function () {
return __object.vueInst
},
setMockMode: function (bol) {
__object.isMockMode = bol
},
openMock: function () { // 开启mock
if (__object.vueInst) {
__object.vueInst.$store.commit('common/SET_IS_MOCK_MODE', true)
this.setMockMode(true)
}
},
cancelMock: function () { // 关闭mock
// mockjs没有提供关闭方法,只能重新刷新页面
location.reload(true)
}
}
})()
// src/store/modules/common/common.js
import * as types from '../../mutation-types'
const state = {
isMockMode: false
}
const getters = {
isMockMode: state => state.isMockMode
}
const actions = {
}
const mutations = {
[types.COMMON_SET_IS_MOCK_MODE] (state, bol) {
state.isMockMode = bol
if (bol) {
// 加载mock
require('@/apiMock/index.js')
}
}
}
export default {
state,
getters,
actions,
mutations
}
// src/store/mutation-type.js
export const COMMON_SET_IS_MOCK_MODE = 'common/SET_IS_MOCK_MODE'
// src/apiMock/index.js
require('./examples/httpUsage')
require('./examples/vuexUsage')
...
通过重载页面关闭mock
由于mockjs
本身并没有提供关闭接口,因此我们只能通过重新加载页面来实现关闭mock(页面初始时mock是关闭的)
// static/global.js
window.myGlobalClosure = (function () {
var __object = {
vueInst: undefined, // vue实例
isMockMode: false, // 是否mock模式,初始为关闭
}
return {
cancelMock: function () { // 关闭mock
// mockjs没有提供关闭方法,只能重新刷新页面
location.reload(true)
}
}
})()
现在,我们可以很方便的在console输入
window.myGlobalClosure.openMock()
来开启mock,输入
window.myGlobalClosure.cancelMock()
关闭mock。
其他: 通过node-Server作为mock服务器
值得说明的是,开发环境中,node作为server,也可以实现请求mock。
// build/http.dev.fake.js
/**
* 开发环境,伪造服务端, 对接 devServer.before
*
*/
module.exports = function (app, server) {
app.post('/postReq1', function (req, res) {
res.json({
'data': {
'cur_page': '这是真实的数据'
}
})
})
app.post('/login', function (req, res) {
res.json({
status: 'success'
})
})
...
}
// build/webpack.dev.conf.js
'use strict'
const baseWebpackConfig = require('./webpack.base.conf')
const httpFake = require('./http.dev.fake')
const devWebpackConfig = merge(baseWebpackConfig, {
...
// these devServer options should be customized in /config/index.js
devServer: {
before: httpFake
},
不过这种方式,mock请求的代码存放在build目录中,而不是src目录,不够规范。另外,mock返回数据的构造,也不如mockjs
插件方便(比如一些随机数等)
路由跳转几乎多有的项目都会用到。这里提供了一个浏览商城的例子,介绍路由设置、跳转、鉴权、状态保存。这里着重说明一下鉴权。
不用的角色访问不同页面,会产生不同的效果。比如普通用户访问后台管理页面会被拒绝,提示无权进入该页面;
游客访问个人主页,会被提示需要登录,并自动跳转到登录页面。
这里鉴权的方法,是在路由对象的meta字段中设置requiresAuth
和role
属性,前者表示访问页面需要授权(即登录),后者表示允许访问的角色。然后,利用vue-router的全局前置守卫拦截路由,进行权限验证和控制。
// src/router/index.js
/**
* 利用全局前置守卫,配合元信息实现路由拦截
* https://router.vuejs.org/zh/guide/advanced/meta.html
*/
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
let role = window.myGlobalClosure.getRole()
if (!role) {
alert(t('page.examples.accessDeny'))
next(to.fullPath.includes('/router') ? {
name: 'routerUsageLogin'
} : {
name: 'vuexUsageLogin'
}) // 一般情况下是跳转到登录页面
} else if (to.meta.role && to.meta.role !== role) {
alert(t('page.examples.accessNotAuthor'))
next(false)
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
vuex是官方推荐的用来做状态管理的一款插件,适合于大中型项目。对于小型项目vue官网给出了一个简单状态管理方式(之前的文档应该是介绍公共总线,现更换成store模式)。
对于是采用vuex还是简单状态管理方式,个人的看法是统一使用其中一种,即要么都采用vuex,要么都采用简单状态管理方式,这样项目整体一致,更容易维护。
本项目模板采用vuex作为全局的状态管理方案。相比于上面介绍的全局闭包,设置vuex变量有这么几个特征:
- 变量
- 多处业务使用到(比如用户登录信息、购物车商品信息,没必要每个使用到的页面都去服务器请求一次)
- 响应式数据(比如在支持多语言的网站,存放当前语言变量,当变量改变,自动触发视图重新渲染)
- 两个子路由组件间数据通信(如果数据量不大,可以用$route.query传递)
vuex的用法例子和vue-router很像,只是多了用户欢迎提示。
这里将用户名称保存为vuex变量,并绑定到多个页面中。值得说明的是,如果vuex变量的更新需要发送http请求,建议通过Action的dispatch方法,因为dispatch允许异步操作,而Mutation的commit方法只能同步执行。
// src/store/modules/pageDemo1/pageDemo1.js
import * as types from '../../mutation-types'
import api from '../../../api/examples/vuexUsage'
import {t} from '../../../lib/locale/index'
const state = {
username: ''
}
const getters = {
getUsername: state => state.username
}
const actions = {
editNickname ({
commit,
getters
}, param) {
return new Promise((resolve, reject) => {
api.setNickname(param.name).then(ret => {
if (ret.data.status === 'success') {
alert(t('page.pageDemo1.setSucc'))
commit(types.COMMON_SET_USERNAME, param.name)
resolve()
} else {
alert(t('page.pageDemo1.setFail'))
reject(new Error(t('page.pageDemo1.setFail')))
}
}).catch(err => {
alert(t('page.pageDemo1.reqUnormal'))
console.log(err)
reject(new Error(t('page.pageDemo1.reqUnormal')))
})
})
}
}
const mutations = {
[types.COMMON_SET_USERNAME] (state, name) {
state.username = name
}
}
// src/api/examples/vuexUsage
import axios from 'axios'
const HTTP = axios
export default {
setNickname (name) {
// 这里的返回是个Promise对象
return HTTP.post('/setNickname', {name: name})
}
}
// 调用
// src/examples/vuexUsage/vChild3.vue
...
methods: {
handleRename() {
this.$store.dispatch('editNickname', {name: this.newname}).then(() => {}).catch(() => {});
}
}
国际化是该项目模板的一大特色,它允许网页按不同的语言进行展示。
国际化的处理思路是:一套html模板,多套语言文件。 以字段$t('字段在语言文件中的路径')
(比如$t('comp.Input.chose')
)替代具体文字(如选择文件
)。系统会根据当前语言,将字段转化为实际内容。
// src/pages/pageDemo1/page.vue
<template>
<article class="usage-demo">
<header>
<h1>{{ msg }}</h1>
<h2>Usage</h2>
</header>
<section>
<ul>
<li>
<!-- {{$t('page.pageDemo1.httpReq')}} 中文对应 http请求, 英文对应 Http request -->
<a @click="routeTo('httpUsage')">{{$t('page.pageDemo1.httpReq')}}</a>
</li>
<li>
<!-- {{$t('page.pageDemo1.routers')}} 中文对应 router路由, 英文对应 Router usage -->
<a @click="routeTo('routerUsageLogin')">{{$t('page.pageDemo1.routers')}}</a>
</li>
<li>
<!-- {{$t('page.pageDemo1.vuexManage')}} 中文对应 vuex状态管理, 英文对应 Vuex management -->
<a @click="routeTo('vuexUsageLogin')">{{$t('page.pageDemo1.vuexManage')}}</a>
</li>
<li>
<!-- {{$t('page.pageDemo1.internation')}} 中文对应 国际化, 英文对应 internation -->
<a @click="routeTo('internUsage')">{{$t('page.pageDemo1.internation')}}</a>
</li>
</ul>
</section>
</article>
</template>
我们采用vue-i18n插件来实现字段转换。
// main.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import App from './App'
// 引入语言文件
import LangEN from './lib/locale/lang/en'
import LangCN from './lib/locale/lang/zh-CN'
Vue.use(VueI18n)
//将语言文件添加到插件实例中
const i18n = new VueI18n({
locale: 'chn', // 默认中文
messages: {
en: LangEN,
chn: LangCN
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
i18n, // 将插件绑定到实例
components: { App },
template: '<App/>'
})
当语言发生改变时,执行下面命令,系统就会自动的将内容转换为该语言下的翻译。
this.$i18n.locale = lang
由于不同语言,字段翻译长度不一样,因而每个页面都需要检查样式是否正常,但是在每个页面甚至局部组件中都暴露语言切换代码(如增加一个切换语言的button),是一件重复且繁琐的事情。因此,我们将语言切换代码直接暴露给全局变量window,从而在Console中输入命令即可实现语言切换。具体的,将语言切换逻辑封装在vuex中,把vue实例赋值给全局闭包window.myGlobalClosure
中的私有变量,通过set方法来执行语言切换。
// src/store/modules/common.js
import * as types from '../../mutation-types'
import {
use
} from '../../../lib/locale/index'
const state = {
i18nLanguage: 'chn' // 系统当前语言是所有页面通用的,可作为CSS类名来调整样式
}
const getters = {
i18nLanguage: state => state.i18nLanguage
}
const actions = {
}
const mutations = {
[types.COMMON_SET_I18N_LANGUAGE] (state, obj) {
let instance = obj.vue
let lang = obj.lang
if (!instance || !lang) {
alert('请传入正确的vue实例和语言:en | chn | turkey')
}
state.i18nLanguage = lang
instance.$i18n.locale = lang
// 更新独立t函数的语言环境(独立t函数见下面说明)
use(instance.$i18n.messages[lang])
}
}
export default {
state,
getters,
actions,
mutations
}
// vueWebTpl\src\store\mutation-types.js
/**
* common
*/
// 国际化语言
export const COMMON_SET_I18N_LANGUAGE = 'common/SET_I18N_LANGUAGE'
// App.vue
export default {
...
mounted() {
// 将实例绑定到全局闭包中
myGlobalClosure.setVueInst(this);
}
}
// static/global.js
window.myGlobalClosure = (function () {
var __object = {
vueInst: undefined, // vue实例
i18nLanguage: 'chn'
}
return {
// 设置vue实例
setVueInst: function (instance) {
__object.vueInst = instance
},
getVueInst: function () {
return __object.vueInst
},
// 设置国际化语言
setLang: function (lang) {
if (__object.vueInst) {
__object.vueInst.$store.commit('common/SET_I18N_LANGUAGE', {
vue: __object.vueInst,
lang: lang
})
}
__object.i18nLanguage = lang
},
getLang: function () {
return __object.i18nLanguage
}
}
})()
现在,在Console中输入
window.myGlobalClosure.setLang('en')
即可快速切换地将项目语言切换到英语。
值得说明的是,如果项目包含了非.vue的js或者jq插件,vue-i18n插件是无法直接切换这些文件的文本字段,因此需要进一步把vue-i18n的翻译函数$t暴露出来。 这里暴露出来的独立t函数,参考了Element-UI的实现,具体见独立t函数
关于国际化处理方案的更多内容,请参考作者的另一篇文档:
vue框架下的国际化
大家在使用项目模板开发项目时,都不可避免的要对其内容进行修改或删除,因为模板提供的demo只是为了供开发人员在开发时参考,上线时这一部分应该去除,否则占据资源和带宽。
考虑到这一点,本项目模板自动提供了生产环境去除demo路由功能,即执行
npm run build
时,不会加载demo页面。另外,demo的路由是懒加载的,因此也不会下载demo组件到浏览器中。因此demo目录可以一直保存在项目中,即使项目维护人员更迭,新人也能看到并参考。
- 通过HtmlWebpackPlugin插件注入环境变量
// webpack.dev.conf.js
const env = require('../config/dev.env')
const definedEnv = env.NODE_ENV // 'development'
...
plugins: [
new HtmlWebpackPlugin({
...
definedEnv: definedEnv
}),
]
// webpack.prod.conf.js
const env = require('../config/prod.env')
const definedEnv = env.NODE_ENV // 'production'
...
plugins: [
new HtmlWebpackPlugin({
...
definedEnv: definedEnv
}),
]
- 在入口html处,将环境变量写入全局闭包
// index.html
<!-- 项目使用到的全局变量 -->
<script type="text/javascript" src="static/global.js"></script>
<script type="text/javascript">
var env = <%=htmlWebpackPlugin.options.definedEnv%>
// 生产环境的路由不包含demo页面
window.myGlobalClosure.setEnv(env)
</script>
// global.js
window.myGlobalClosure = (function () {
var __object = {
...
env: 'development', // 当前环境
}
return {
getEnv: function () {
return __object.env
},
setEnv: function (env) {
__object.env = env
},
...
}
})
- 路由表中根据当前环境决定是否导入demo页面
// src/router/index.js
import page1Router from './pageDemo1.router'
import InputMock from '@/components/InputMock'
Vue.use(Router)
let env = window.myGlobalClosure.getEnv()
const routes = env != 'production' ? [
{
path: '/inputMock',
name: 'inputMock',
component: InputMock
},
...page1Router
] : []
- demo路由懒加载
// 懒加载路由,不影响实际加载性能
// https://router.vuejs.org/zh/guide/advanced/lazy-loading.html#%E6%8A%8A%E7%BB%84%E4%BB%B6%E6%8C%89%E7%BB%84%E5%88%86%E5%9D%97
const httpUsage = () => import('../examples/httpUsage.vue')
export default [{
path: '/examples',
component: {
template: '<article><router-view></router-view></article>'
},
children: [{
path: 'http',
name: 'httpUsage',
component: httpUsage
},
...]
}]
为了加快开发和部署,我们提供了一些小工具。
auto-commit.sh
关于脚本的具体说明,请参考作者的另一篇文档:
自动提交webapp
在开发多语言版本时,文本翻译有时候是挺费时且容易出错的事情。为此,我们的项目模板提供了自动导入导出语言包工具。
关于工具的具体说明,请参考作者的另一篇文档:
自动导入导出语言包
- 通用组件库划分
- webpack4特性引入
- 图片处理
- 组件和样式分离
- BEM语法
- 第三方js、jq插件的国际化