基于 vue.js 的前端开发环境,用于前后端分离后的单页应用开发,可以在开发时使用 ES Next、scss 等最新语言特性。项目包含:
基础库: vue.js、vue-router、vuex、whatwg-fetch
编译/打包工具:webpack、babel、node-sass
单元测试工具:karma、mocha、sinon-chai
本地服务器:express
目录结构
├── README.md 项目介绍
├── index.html 入口页面
├── build 构建脚本目录
│ ├── build-server.js 运行本地构建服务器,可以访问构建后的页面
│ ├── build.js 生产环境构建脚本
│ ├── dev-client.js 开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新
│ ├── dev-server.js 运行本地开发服务器
│ ├── utils.js 构建相关工具方法
│ ├── webpack.base.conf.js wabpack基础配置
│ ├── webpack.dev.conf.js wabpack开发环境配置
│ └── webpack.prod.conf.js wabpack生产环境配置
├── config 项目配置
│ ├── dev.env.js 开发环境变量
│ ├── index.js 项目配置文件
│ ├── prod.env.js 生产环境变量
│ └── test.env.js 测试环境变量
├── mock mock数据目录
│ └── hello.js
├── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息
├── src 源码目录
│ ├── main.js 入口js文件
│ ├── app.vue 根组件
│ ├── components 公共组件目录
│ │ └── title.vue
│ ├── assets 资源目录,这里的资源会被wabpack构建
│ │ └── images
│ │ └── logo.png
│ ├── routes 前端路由
│ │ └── index.js
│ ├── store 应用级数据(state)
│ │ └── index.js
│ └── views 页面目录
│ ├── hello.vue
│ └── notfound.vue
├── static 纯静态资源,不会被wabpack构建。
└── test 测试文件目录(unit&e2e)
└── unit 单元测试
├── index.js 入口脚本
├── karma.conf.js karma配置文件
└── specs 单测case目录
└── Hello.spec.js
环境安装
本项目依赖 node.js, 使用前先安装 node.js 和 cnpm(显著提升依赖包的下载速度)。
自行下载并安装 node.js: https://nodejs.org/en/download/
然后安装 cnpm 命令:
npm install -g cnpm --registry=https://registry.npm.taobao.org
快速开始
git clone https://github.com/hanan198501/vue-spa-template.git
cd vue-spa-template
cnpm install
npm run dev
命令列表:
开启本地开发服务器,监控项目文件的变化,实时构建并自动刷新浏览器,浏览器访问 http://localhost:8081
npm run dev
使用生产环境配置构建项目,构建好的文件会输出到 "dist" 目录,
npm run build
运行构建服务器,可以查看构建的页面
npm run build-server
运行单元测试
npm run unit
前后端分离
项目基于 spa 方式实现前后端分离,服务器通过 nginx 区分前端页面和后端接口请求,分发到不同服务。前端物理上只有一个入口页面, 路由由前端控制(基于vue-router),根据不同的 url 加载相应数据和组件进行渲染。
接口 mock
前后端分离后,开发前需要和后端同学定义好接口信息(请求地址,参数,返回信息等),前端通过 mock 的方式,即可开始编码,无需等待后端接口 ready。 项目的本地开发服务器是基于 express 搭建的,通过 express 的中间件机制,我们已经在 dev-server 中添加了接口 mock 功能。 开发时,接口的 mock 数据统一放在 mock 目录下,每个文件内如下:
module.exports = {
// 接口地址
api: '/api/hello',
// 返回数据 参考http://expressjs.com/zh-cn/4x/api.html
response: function (req, res) {
res.send(`
hello vue!
`);
}
}
模块化
开发时可以使用 ES2015 module 语法,构建时每个文件会编译成 amd 模块。
组件化
整个应用通过 vue 组件的方式搭建起来,通过 vue-router 控制相应组件的展现,组件树结构如下:
app.vue 根组件(整个应用只有一个)
├──view1.vue 页面级组件,放在 views 目录里面,有子组件时,可以建立子目录
│ ├──component1.vue 功能组件,公用的放在 components 目录,否则放在 views 子目录
│ ├──component2.vue
│ └──component3.vue
├──view2.vue
│ ├──component1.vue
│ └──component4.vue
└──view3.vue
├──component5.vue
……
单元测试
可以为每个组件编写单元测试,放在 test/unit/specs 目录下面, 单元测试用例的目录结构建议和测试的文件保持一致(相对于src),每个测试用例文件名以 .spec.js结尾。 执行 npm run unit 时会遍历所有的 spec.js 文件,产出测试报告在 test/unit/coverage 目录。
components
文件夹用来存放Vue 组件。个人建议,把每一个组件中使用到的image图片放置到对应的组件子文件目录下,便于统一的管理
Node_modules/npm
安装的该项目的依赖库
vuex
/文件夹存放的是和 Vuex store 相关的东西(state对象,actions,mutations)
router
/文件夹存放的是跟vue-router相关的路由配置项
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: 路由配置项
*
* hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
* alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
* // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
* // 若你想不管路由下面的 children 声明的个数都显示你的根路由
* // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : {
noCache: true // 如果设置为true,则不会被 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
}
*/
// 公共路由
export const constantRoutes = [
{
path: '/redirect',
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect')
}
]
},
{
path: '/login',
component: () => import('@/views/login'),
hidden: true
},
{
path: '/register',
component: () => import('@/views/register'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/error/404'),
hidden: true
},
{
path: '/401',
component: () => import('@/views/error/401'),
hidden: true
},
{
path: '',
component: Layout,
redirect: 'index',
children: [
{
path: 'index',
component: () => import('@/views/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{
path: '/user',
component: Layout,
hidden: true,
redirect: 'noredirect',
children: [
{
path: 'profile',
component: () => import('@/views/system/user/profile/index'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user' }
}
]
}
]
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
{
path: '/system/user-auth',
component: Layout,
hidden: true,
permissions: ['system:user:edit'],
children: [
{
path: 'role/:userId(\\d+)',
component: () => import('@/views/system/user/authRole'),
name: 'AuthRole',
meta: { title: '分配角色', activeMenu: '/system/user' }
}
]
},
{
path: '/system/role-auth',
component: Layout,
hidden: true,
permissions: ['system:role:edit'],
children: [
{
path: 'user/:roleId(\\d+)',
component: () => import('@/views/system/role/authUser'),
name: 'AuthUser',
meta: { title: '分配用户', activeMenu: '/system/role' }
}
]
},
{
path: '/system/dict-data',
component: Layout,
hidden: true,
permissions: ['system:dict:list'],
children: [
{
path: 'index/:dictId(\\d+)',
component: () => import('@/views/system/dict/data'),
name: 'Data',
meta: { title: '字典数据', activeMenu: '/system/dict' }
}
]
},
{
path: '/monitor/job-log',
component: Layout,
hidden: true,
permissions: ['monitor:job:list'],
children: [
{
path: 'index',
component: () => import('@/views/monitor/job/log'),
name: 'JobLog',
meta: { title: '调度日志', activeMenu: '/monitor/job' }
}
]
},
{
path: '/tool/gen-edit',
component: Layout,
hidden: true,
permissions: ['tool:gen:edit'],
children: [
{
path: 'index/:tableId(\\d+)',
component: () => import('@/views/tool/gen/editTable'),
name: 'GenEdit',
meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
}
]
}
]
// 防止连续点击多次路由报错
let routerPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return routerPush.call(this, location).catch(err => err)
}
export default new Router({
mode: 'history', // 去掉url中的#
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
build
/文件是 webpack 的打包编译配置文件
static
/文件夹存放一些静态的、较少变动的image或者css文件
config
/文件夹1.存放的是一些配置项,比如服务器访问的端口配置等
2.vue3的项目没有config文件夹
index.js
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
// 引入 node.js 的路径模块
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
// 建一个虚拟 api 服务器用来代理本机的请求,只能用于开发模式
proxyTable: {},
// 服务 host
host: 'localhost',
// 服务端口号
port: 8080,
// 自动打开浏览器浏览器
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// 下面是相对路径的拼接,假如当前跟目录是 config,那么下面配置的 index 属性的属性值就是 dist/index.html
index: path.resolve(__dirname, '../dist/index.html'),
// 定义的是静态资源的根目录 也就是 dist 目录
assetsRoot: path.resolve(__dirname, '../dist'),
// 定义的是静态资源根目录的子目录 static,也就是 dist 目录下面的 static
assetsSubDirectory: 'static',
// 定义的是静态资源的公开路径,也就是真正的引用路径
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
// 是否压缩代码
productionGzip: false,
// 定义要压缩哪些类型的文件
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
// 编译完成后的报告,可以通过设置值为 true 和 false 来开启或关闭
bundleAnalyzerReport: process.env.npm_config_report
}
}
dev.env.js
'use strict'
// 配置文件合并模块
const merge = require('webpack-merge')
// 导入 prod.env.js 配置文件
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
// 导出一个对象,NODE_ENV 是一个环境变量,指定 development 环境
NODE_ENV: '"development"'
})
prod.env.js
'use strict'
module.exports = {
// 导出一个对象,NODE_ENV 是一个环境变量,指定 production 环境
NODE_ENV: '"production"'
}
test.env.js
'use strict'
// 配置文件合并模块
const merge = require('webpack-merge')
// 导入 dev.env.js 配置文件
const devEnv = require('./dev.env')
module.exports = merge(devEnv, {
// 导出一个对象,NODE_ENV 是一个环境变量,指定 testing 环境
NODE_ENV: '"testing"'
})
dist
/文件夹一开始是不存在,在我们的项目经过 build 之后才会产出
App.vue
根组件1.所有的子组件都将在这里被引用
2.作为单页面组件,只要写一个
就好了,所有的页面都会在这儿进行切换
index.html
整个项目的入口文件,将会引用我们的根组件 App.vue
main.js
入口文件的 js 逻辑,在webpack 打包之后将被注入到 index.html 中
//导入第三方库 vue
import Vue from 'vue'
//导入第三方库 js-cookie
import Cookies from 'js-cookie'
//导入第三方库 Element
import Element from 'element-ui'
//导入一个css
import './assets/styles/element-variables.scss'
//导入一个css
import '@/assets/styles/index.scss' // global css
//导入一个css
import '@/assets/styles/ruoyi.scss' // ruoyi css
//导入本地文件 APP.VUE
import App from './App'
//导入本地文件夹store, 默认寻找index.vue
import store from './store'
//导入本地文件夹router, 默认寻找index.vue
import router from './router'
//导入本地文件夹directive , 默认寻找index.vue
import directive from './directive' // directive
//导入本地文件夹plugins , 默认寻找index.vue
import plugins from './plugins' // plugins
//导入本地文件request,解构download 方法
import { download } from '@/utils/request'
//导入本地文件夹icons
import './assets/icons' // icon
//导入本地文件夹permission
import './permission' // permission control
//导入本地文件data,解构getDicts 方法
import { getDicts } from "@/api/system/dict/data";
//导入本地文件config,解构getConfigKey方法
import { getConfigKey } from "@/api/system/config";
//导入本地文件ruoyi,解构这些方法
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
// 分页组件,默认寻找index.vue
import Pagination from "@/components/Pagination";
// 自定义表格工具组件,默认寻找index.vue
import RightToolbar from "@/components/RightToolbar"
// 富文本组件,默认寻找index.vue
import Editor from "@/components/Editor"
// 文件上传组件,默认寻找index.vue
import FileUpload from "@/components/FileUpload"
// 图片上传组件,默认寻找index.vue
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件,默认寻找index.vue
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件,默认寻找index.vue
import DictTag from '@/components/DictTag'
// 头部标签组件,默认寻找index.vue
import VueMeta from 'vue-meta'
// 字典数据组件,默认寻找index.vue
import DictData from '@/components/DictData'
// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
//Vue 引入插件( 要想用Vue.use()的方式,首先你得将组件转换成插件的形式才可以,需要额外在src下建立一个plugin的文件夹,将自己封装的组件引入到该文件夹下(也可以不引入,引入了比较方便些),然后再建立一个index.js的文件)
Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
DictData.install()
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
//去除错误提示
Vue.config.productionTip = false
//挂载路由,vuex到APP上
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
vue.config.js
1.设置自动打开和端口号
2.process.env.NODE_ENV表示现在的所处的环境,开发环境和打包环境
// const path = require('path');
module.exports = {
/** 区分打包环境与开发环境
* process.env.NODE_ENV==='production' (打包环境)
* process.env.NODE_ENV==='development' (开发环境)
* baseUrl: process.env.NODE_ENV==='production'?"https://xxx":'',
*/
// 项目部署的基础路径
// 我们默认假设你的应用将会部署在域名的根部,
// 例如 https://www.my-app.com/
// 如果你的应用部署在一个子路径下,那么你需要在这里
// 指定子路径。比如将你的应用部署在
// https://www.foobar.com/my-app/
// 那么将这个值改为 '/my-app/'
//baseUrl: '/',//vue-cli3.3以下版本使用
publicPath: '/',//vue-cli3.3+新版本使用
// 构建好的文件输出到哪里
outputDir: "dist",
// assetsDir: "base" //静态资源打包地址
//以多页模式构建应用程序。
pages: undefined,
// 是否在保存时使用‘eslint-loader’进行检查 // 有效值: true | false | 'error'
// 当设置为‘error’时,检查出的错误会触发编译失败
lintOnSave: true,
// 使用带有浏览器内编译器的完整构建版本,是否使用包含运行时编译器的 Vue 构建版本
runtimeCompiler: false,
// babel-loader默认会跳过`node_modules`依赖. // 通过这个选项可以显示转译一个依赖
// 默认babel-loader忽略mode_modules,这里可增加例外的依赖包名
transpileDependencies: [],
// 是否在构建生产包时生成 sourceMap 文件,false将提高构建速度 映射文件 打包时使用
productionSourceMap: false,
// 调整内部的webpack配置.
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
chainWebpack: () => { },
// chainWebpack: () => {
// // 删除懒加载模块的prefetch,降低带宽压力
// // 而且预渲染时生成的prefetch标签是modern版本的,低版本浏览器是不需要的
// //config.plugins.delete('prefetch');
// //if(process.env.NODE_ENV === 'production') {
// // 为生产环境修改配置...process.env.NODE_ENV !== 'development'
// //} else {
// // 为开发环境修改配置...
// //}
// },
configureWebpack: () => { },
// configureWebpack: () => {
// // 生产and测试环境
// let pluginsPro = [
// new CompressionPlugin({ //文件开启Gzip,也可以通过服务端(如:nginx)
// filename: '[path].gz[query]',
// algorithm: 'gzip',
// test: new RegExp('\\.(' + ['js', 'css'].join('|') + ')$'),
// threshold: 8192,
// minRatio: 0.8,
// }),
// new BundleAnalyzerPlugin(),
// ];
// //开发环境
// let pluginsDev = [
// new vConsolePlugin({
// filter: [], // 需要过滤的入口文件
// enable: true // 发布代码前记得改回 false
// }),
// ];
// if (process.env.NODE_ENV === 'production') { // 为生产环境修改配置...process.env.NODE_ENV !== 'development'
// config.plugins = [...config.plugins, ...pluginsPro];
// } else {
// // 为开发环境修改配置...
// config.plugins = [...config.plugins, ...pluginsDev];
// }
// },
// CSS 相关选项
css: {
// 将组件内部的css提取到一个单独的css文件(只用在生产环境)
// 也可以是传递给 extract-text-webpack-plugin 的选项对象
// 是否使用css分离插件 ExtractTextPlugin,采用独立样式文件载入,不采用