基于 vue & vuex & vue-router & axios & element-ui 的微前端子应用脚手架工程模板
|── build // 构建相关
├── config // 配置相关
│ ├── dev.env.js // 开发环境
│ ├── index.js // 项目配置(配置测试环境代理请求地址)
│ ├── prod.env.js // 生产环境
├── src // 源代码
│ ├── api // 所有 HTTP 请求
│ │ ├── home.js // 首页(根据业务模块命名,和 /views/* 一一对应)
│ ├── assets // 图片样式等静态资源
│ ├── mixins // 混入对象
│ │ ├── el-form-item.js // 表单项验证增强
│ │ ├── index.js // 定义并导出混入对象的组件
│ ├── components // 全局公用组件
│ ├── router // 路由
│ │ ├── asyncComponents.js // 异步载入的组件
│ │ ├── components.js // 同步载入的组件(设置会被编译到本app.js中的组件)
│ │ ├── index.js // 路由入口
│ │ ├── menu.js // 菜单设置(设置所有一级、二级、三级菜单)
│ │ ├── routes.js // 路由设置(设置所有路由对应组件别名)
│ ├── store // 全局 store 管理
│ │ ├── modules // 模块
│ │ │ ├── user.js // 用户(定义 user 模块)
│ │ ├── getters.js // 定义 store 计算属性(定义 store 计算属性,以逗号分割增加)
│ │ ├── index.js // 组装 store 并导出(组装 user 并导出)
│ ├── utils // 全局公用方法
│ │ ├── auth.js // 操作 token
│ │ ├── cookies.js // 操作 cookies
│ │ ├── index.js // 公用方法
│ │ ├── request.js // 全局 http 请求方法封装
│ │ ├── storage.js // 全局 storage 相关方法封装
│ ├── views // 页面视图
│ │ ├── home // 响应路由切换的 vue 组件(根据业务模块命名,和 /api/* 一一对应)
│ ├── app.js // vue 入口加载组件初始化等
│ ├── config.js // 微服务 id 存放在这个文件中,webpack、server 等 js 都有读取这个id配置
│ ├── service.js // Portal 入口加载组件初始化等
├── theme // 主题文件目录
├── element-variables.scss // 主题变量文件
└── package.json // 包配置
可只关注有括号说明的目录或文件,文件内有括号中一样的注释,比如:
├── config // 配置相关
│ ├── index.js // 项目配置(配置测试环境代理请求地址)
// 配置测试环境代理请求地址
proxyTable: {
// 例如将'localhost:8080/api/user'代理到'http://rap2api.taobao.org/app/mock/95082/user'
'/api': {
target: 'http://rap2api.taobao.org/app/mock/95082/', // 接口的域名
secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
pathRewrite: { '^/api': '' } // pathRewrite 来重写地址,将前缀 '/api' 转为 '/'。
},
},
# 安装依赖
npm install
# 进入开发模式,启动前台应用,localhost:3000,监听vue文件改动自动刷新浏览器
npme start
# 构建文件到dist目录供发布,无法单独部署访问,必须集成到portal访问
npm run build
如果一切顺利,就能正常打开端口: http://localhost:3000/
npm run <script> |
解释 |
---|---|
dev |
打包测试资源 |
build |
打包正式资源 |
start |
启动3000端口服务 |
- 开发使用
dev/dev:ie 和 start
- 发布使用
build
- 访问 海关服务市场 填写配置后,点击下载按钮,下载本脚手架前端工程,确认
src/config.js
里的 SERVICEID 和 SERVICENAME 配置是否正确; - 根据项目实际需求,配置组件
src/router/components.js
、路由src/router/router.js
、菜单src/router/menu.js
、HTTP 请求代理地址/config/index.js
,准备各路由所对应的 vue 文件(例如:src/views/home.js
,请根据业务模块命名),分配给项目成员实现; - 访问 可视化接口管理工具 配置 mock 数据;
- 实现 vue 文件的界面部分;
- 后端实现 RESTful 接口,并维护接口文档;
- 调试后端接口;
- 测试。
文件地址:/src/router/components.js
// 设置会被编译到本app.js中的组件
const components = {
'/home/index': () => import('@/views/home/index.vue'),
'/home/element/message': () => import('@/views/home/element/message.vue'),
'/home/element/notice': () => import('@/views/home/element/notice.vue')
}
// 设置路由
const routes = [
{
path: `/${SERVICEID}/home/index`,
component: '/home/index',
},
{
path: `/${SERVICEID}/home/element/message`,
component: '/home/element/message',
},
{
path: `/${SERVICEID}/home/element/notice`,
component: '/home/element/notice',
}
]
文件地址:/src/router/menu.js
// 设置所有一级、二级、三级菜单
const menuData = [
{
path: `/${SERVICEID}/home/index`,
meta: { title: '首页', icon: 'fa fa-home' } // icon 支持 font-awesome 和 element
},
{
path: `/${SERVICEID}/home/element/message`,
meta: { title: '示例', icon: 'fa fa-inbox' },
children: [
{
path: `/${SERVICEID}/home/element/message`,
meta: { title: 'element', icon: 'el-icon-star-on' },
children: [
{
path: `/${SERVICEID}/home/element/message`,
meta: { title: '消息提示', icon: 'fa fa-comment' }
},
{
path: `/${SERVICEID}/home/element/notice`,
meta: { title: '通知', icon: 'fa fa-bell' }
}
]
}
]
}
]
文件地址:/config/index.js
// 配置测试环境代理请求地址
proxyTable: {
// 例如将'localhost:8080/api/xxx'代理到'http://api.xxx.cn/xxx'
'/api': {
target: 'http://rap2api.taobao.org/app/mock/95082/', // 接口的域名
secure: false, // 如果是https接口,需要配置这个参数
changeOrigin: true, // 如果接口跨域,需要进行这个参数配置
pathRewrite: { '^/api': '' } // pathRewrite 来重写地址,将前缀 '/api' 转为 '/'。
},
},
文件地址:/api/home.js
import request from '@/utils/request'
// 根据业务模块命名,和 /views/* 一一对应
export function fakeUser (params) {
// 创建用户
return request({
url: '/api/user', // 通过 /config/index.js 中 proxyTable 代理转发请求
method: 'POST',
data: params
})
}
文件地址:/src/views/home/index.vue
<template>
<div class="hello">
<p>头像:<img :src="user.avatar" height="50" /></p>
<p>角色:</p>
<p>用户名:</p>
<p>介绍:</p>
</div>
</template>
data() {
return {
msg: '欢迎使用',
user: {
id:'82073',
roles: '',
name: '',
introduction: '',
avatar: '',
},
}
},
methods: {
// 调用http请求
getUser() {
// 获取用户
queryUser(this.user.id).then(response => {
const { roles, name, avatar, introduction } = response.data
// 业务逻辑
this.user.name = name
this.user.roles = roles
this.user.introduction = introduction
this.user.avatar = avatar
})
},
文件地址:/src/views/home/index.vue
<template>
<div class="hello">
<p>头像:<img :src="avatar" height="50" /></p>
<p>角色:</p>
<p>用户名:tax</p>
<p>介绍:</p>
</div>
</template>
data() {
return {
msg: '欢迎使用',
user: {
id:'82073',
roles: '',
name: '',
introduction: '',
avatar: '',
},
paraCreate: {
username: 'michael',
password: '123456',
},
paraEdit: {
username: 'jordan',
},
}
},
methods: {
// vuex 写法
getUser() {
// 任何状态的改变都是通过触发 action 开始
Store.dispatch('GetUserInfo').then(response => {
const { status, statusText, headers, data } = response
})
},
// 通过计算属性返回状态
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters(['name', 'avatar', 'introduction', 'roles'])
}
文件地址:/src/store/index.js
import user from './modules/user' // 导入 user 模块
// 组装 user 并导出
const store = new Vuex.Store({
modules: {
user // 对应上面导入的 user 模块,以逗号分割增加组装
},
getters
})
const getters = {
// 定义 store 计算属性,以逗号分割增加
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name,
introduction: state => state.user.introduction,
roles: state => state.user.roles
}
文件地址:/src/store/modules/user.js
import { getUserInfo } from '@/api/home'
import { getToken } from '@/utils/auth'
const user = {
state: {
// vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态
user: '',
userid: '82073',
token: getToken(),
name: '',
avatar: '',
introduction: '',
roles: []
},
mutations: {
// 类似事件,实际进行状态更改的地方
SET_TOKEN: (state, token) => {
state.token = token
},
SET_INTRODUCTION: (state, introduction) => {
state.introduction = introduction
},
SET_NAME: (state, name) => {
state.name = name
},
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_ROLES: (state, roles) => {
state.roles = roles
}
},
actions: {
// 异步逻辑都应该封装到 action 里面,业务逻辑写在视图里
GetUserInfo({ commit, state }) {
// 获取用户信息
return new Promise((resolve, reject) => {
queryUser(state.userid)
.then(response => {
const { roles, name, avatar, introduction } = response.data
if (roles && roles.length > 0) {
// 验证返回的roles是否是一个非空数组
commit('SET_ROLES', roles)
} else {
reject(new Error('getInfo: roles must be a non-null array !'))
}
commit('SET_NAME', name) // 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的
commit('SET_AVATAR', avatar)
commit('SET_INTRODUCTION', introduction)
resolve(response)
})
.catch(error => {
reject(error)
})
})
}
}
}
# 改变主题色变量
vim element-variables.scss
$--color-primary: #FF003C !default;
# 生成主题文件目录,使修改生效
node_modules/.bin/et -w // -w 参数是监听文件改动自动编译
$ npm run build // 打包文件为 build 文件夹,请以此为根目录
- /api/*.js 所有 js 都用驼峰命名,并且内部注释该 js 功能,删除未用 js;
- 根据业务模块来划分 views,并且将 views 和 api 两个模块一一对应,方便维护;
- 独立的东西,没有必要使用 vuex 来存储 data,每个页面里存放自己的 data 就行。但如登录 token,用户信息,或者是一些全局个人偏好设置等,还是用vuex管理更加的方便,具体还是要结合业务场景;
- 后端返回前端的数据,字段名同数据库中的字段名,并转为小写字母开头的驼峰式命名,构造 mock 数据时也要注意这一点;
- 工程编译时,
src
目录下的assets
目录下的文件会被直接复制到 dist 目录下; @
是src
的别名,在程序中引入路径的时候,@/utils/request
就直接可以代替../src/utils/request
;- 为了便于维护,对话框、页签等如果里面的内容比较多(超过30行),要独立成 vue 组件,尽量不要让一个 vue 组件的代码太多(超过500行超过20K),尽量把 vue 文件里的 js 移到单独的文件,便于使用编辑器的 js 校验 js 格式化功能。vue 文件中 css 代码行数较多时(超过50行),亦可将 css 移到单独的 css 文件。模板部分要保持在 vue 文件里,以使用 Vetur 插件的模板语法校验功能。
- 使用 VSCode 作为 js/vue 的编辑器,并安装以下插件
EditorConfig for VSCode
,Prettier-Standard - JavaScript formatter
,JavaScript Standard Style
,stylefmt
,Vetur
; - 在 VSCode 的配置里要加下面的命令,格式化时使用单引号而不是双引号和防止自动加分号:
"prettier.singleQuote": true
,"prettier.semi": false
- 可修改
/src/app.js
中的import App from '@/views/main.vue'
来去左侧菜单; - 安装并配置 ESLint(可组装的JavaScript和JSX检查工具,保持团队代码规范统一),依次点击 文件 > 首选项 > 设置 打开 VSCode 配置文件,添加如下配置
"eslint.validate": [
"javascript",
"javascriptreact",
"html",
{
"language": "vue",
"autoFix": true
}
],
"eslint.options": {
"plugins": ["html"]
},
"eslint.autoFixOnSave": true,
"eslint.alwaysShowStatus": true,
"prettier.semi": false,
"prettier.singleQuote": true,
"prettier.jsxSingleQuote": true,
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatterOptions": {
"wrap_attributes": "force-aligned"
}
子应用创建的全局方法或变量,都要放到子应用ID的命名空间下,例如:
import {SERVICEID} from './config.js'
let ns = window[SERVICEID] = window[SERVICEID] || {}
ns.foo = { bar: 'val1', baz: 'val2'}
ns.qux = (a, b) => {}
在脚手架工程下提供sessionStorage的前后端操作库(src/utils/storage.js
),接管sessionStorage操作。
import {SERVICEID} from './config.js'
import SessionStorage from './utils/storage.js'
SessionStorage.setServiceId(SERVICEID) // 设置子应用id
SessionStorage.setItem('foo', 'value')
在脚手架工程下提供localStorage的前后端操作库(src/utils/storage.js
),接管localStorage操作。
import {SERVICEID} from './config.js'
import LocalStorage from './utils/storage.js'
LocalStorage.setServiceId(SERVICEID) // 设置子应用id
LocalStorage.setItem('foo', 'value')
在脚手架工程下提供cookie的前后端操作库(src/utils/cookies.js
),接管cookie操作。
import { SERVICEID } from './config.js'
import Cookies from './utils/cookies.js'
// 操作 Cookies 是往浏览器 LocalStorage 里存值,通过 key 来区分,为了防止多个子应用设置 cookies 超量
Cookies.setServiceId(SERVICEID) // 设置子应用id
Cookies.set('name', 'value'); // 永不过期
Cookies.set('name', 'value', { expires: 7 }); // 过期时间7天
Cookies.get('name'); // => 'value'
Cookies.remove('name');
Cookies.get('name'); // => undefined
-
在
portal framework
里我们定义了portal.global
为一个Observable
实例,Observable
是订阅/发布模式
的实现,所以portal.global
支持get``set``subscribe``unsubscribe
等方法function callback(value, path) { console.log(value, path); } portal.global.set('foo', { bar: 'value' },true) // 改变属性foo的值,且不允许被订阅 portal.global.set('foo.bar', 'newValue') // 改变属性 foo.bar的值 portal.global.get('foo') // 获取属性foo 的值 portal.global.get('foo.bar') // 获取属性bar 的值 portal.global.subscribe('foo', callback) // 监听foo属性的改变 portal.global.subscribe('foo.bar', callback) // 监听bar属性的改变 portal.global.unsubscribe('foo', callback) // 停止监听 foo 属性的改变 portal.global.unsubscribe('foo.bar', callback) // 停止监听 bar 属性的改变
-
子应用可以在 bootstrap 生命周期函数中,声明自己的可以被订阅的跨服务数据项,声明方式类似于:
portal.global.set("form.count",1,false)
-
其他子界面可以在自己的bootstrap生命周期函数中声明要监听跨服务业数据项。声明方式类似于:
portal.global.subscribe("form.count",function(value, path){ //获取value做相应动作 });
- 子应用里的链接要设置属性
target="_blank"
,使页面在新窗口打开。 - 集成页面的js会遍历没有设置属性为
target="_blank"
的链接,并禁止这些链接在当前窗口打开。
在集成页面已经引入element-ui
,并且会对element-ui
样式作统一调整,因此所有子应该尽量使用element-ui
样式,并且不要自己再引入element-ui
。参见element-ui
文档 http://element.eleme.io
只是基于 element UI 的 el-form-item 原本的校验方式做了一层封装,主要让表单项的验证编码更快捷。
在 el-form-item 的 props 上新增了 一个 verify,非侵入式,完全不会影响你继续使用ElementUI的原生校验。
源代码在: /src/mixins/el-form-item.js
<template>
<el-form :model="form" ref="numberForm" label-width="100px">
<el-form-item label="名称" prop="name" :verify="['NotNull', 'Length=3']">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="年龄" prop="age" :verify="['NotNull', 'Number']">
<el-input v-model="form.age"></el-input>
</el-form-item>
<el-form-item label="电子邮箱" prop="email" :verify="['NotNull', 'Email']">
<el-input v-model="form.email"></el-input>
</el-form-item>
<el-form-item label="生日" prop="sr" :verify="['NotNull', 'DateTime=yyyy年MM月dd日']">
<el-input v-model="form.sr"></el-input>
</el-form-item>
<el-form-item label="手机号码" prop="phone" :verify="['NotNull', 'CnPhone']">
<el-input v-model="form.phone"></el-input>
</el-form-item>
<el-form-item label="备注" prop="bz" :verify="['NotNull', 'Regex=[a-z]{10,}']">
<el-input v-model="form.bz"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">提交</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data () {
return {
dataLoading: true,
detectors: [],
form: {
age: '1',
name: '',
email: '',
sr: '',
phone: '',
bz: ''
}
}
},
}
</script>
参数 | 说明 | 类型 | 默认值 | required |
---|---|---|---|---|
verify | 表单项,验证设置 | Array | [] |
设置项 | 说明 | 是否需要参数 | 示例 |
---|---|---|---|
NotNull | 不能为空 | 不需要 | 'NotNull' |
Number | 必须为数字 | 不需要 | 'Number |
Int | 必须为整数 | 不需要 | 'Int' |
DateTime | 必须是日期时间 | 日期时间格式 | 'DateTime=yyyy年MM月dd日' |
必须是Email | 不需要 | 'Email' | |
ZipCode | 必须是邮政编码 | 不需要 | 'ZipCode' |
CnTel | 必须是固定电话 | 不需要 | 'CnTel' |
CnPhone | 必须是手机号码 | 不需要 | 'CnPhone' |
IDCardNo | 必须身份证号码 | 不需要 | 'IDCardNo' |
Length | 长度等于、大于、小于n | 长度 | 'Length=3', 'Length>3', 'Length<3' |
Regex | 必须符合正则表达式 | 正则表达式 | 'Regex=[a-z]{1,}' 'Regex=^\\w+$|只能是数字或英文字母' |