一、 目录结构
根目录下设置两个目录,分别承载后端和前端两个工程。
(一) 后端
后端工程目录结构如下。
后端工程源自默认生成的express工程,目录结构如下:
【bin】
bin目录下保存的www文件是执行入口。启动方式是:node ./www
【node_modules】
【public】
程序将此目录挂载为’/’,前端工程中生成的文件均放置于此。
【routes】
存放路由文件。
【app.js】
app.js是主入口文件,这个文件中主要修改了路由。
【package.json】
(二) 前端
前端使用webpack打包,将结果输出为文件,使用Vue.js和Element UI组件。目录结构如下。
【dst目录】
存放各模块生成的目标js文件。
目录下设置index.html文件,用于测试,其余文件均为程序生成文件。
【node_modules目录】
【src目录】
存放源码,以子目录方式分割各模块。
模块子目录下有app.js、app.vue以及其他所需文件。
【package.json】
【webpack.common.js】
【webpack.dev.js】
【webpack.prod.js】
二、 源码实现
(一) 配置文件
【前端package.json】
修改内容如下:
修改scripts节,编写webpack打包脚本,增加babel-loader的配置。
{ "name": "xxx-frontend", "version": "1.0.0", "description": "xxx", "scripts": { "dev": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.prod.js" }, "author": "wangxq", "license": "ISC", "devDependencies": { "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-es2015": "^6.24.1", "clean-webpack-plugin": "^0.1.17", "css-loader": "^0.28.7", "file-loader": "^0.11.2", "style-loader": "^0.18.2", "uglifyjs-webpack-plugin": "^0.4.6", "vue-loader": "^13.0.5", "vue-template-compiler": "^2.4.4", "webpack": "^3.6.0", "webpack-dev-server": "^2.8.2", "webpack-merge": "^4.1.0" }, "dependencies": { "element-ui": "^1.4.6", "vue": "^2.4.4" }, "babel": { "presets": [ "es2015" ] } }
【webpack.common.js】
webpack配置。
const path = require('path'); module.exports = { entry: { // 模块:desktop desktop: './src/desktop/app.js', // 模块:login login: './src/login/app.js', }, output: { // 输出文件名:模块名.dst.js filename: '[name].dst.js', // 输出路径:当前路径下的dst目录 path: path.resolve(__dirname, 'dst') }, module: { rules: [ { // vue-loader,处理vue文件 test: /\.vue$/, loader: 'vue-loader' }, { // vue-loader默认使用babel-loader处理js test: /\.js$/, loader: 'babel-loader', exclude: path.join(__dirname, './node_modules') }, { test: /\.css$/, loader: 'style-loader!css-loader' }, { test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/, loader: 'file-loader', query: { // 目标路径是/ttfs,生成文件时会在dst目录下建立ttfs目录,将文件拷贝至后端public/ttfs目录下即可 name: '/ttfs/[name].[ext]?[hash]' } }, { test: /\.(png|jpe?g|gif|svg)(\?\S*)?$/, loader: 'file-loader', query: { // 目标路径是/images,生成文件时会在dst目录下建立images目录,将文件拷贝至后端public/images目录下即可 name: '/images/[name].[ext]?[hash]' } } ] } };
【webpack.dev.js】
const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { devServer: { contentBase: './dst' } });
【webpack.prod.js】
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const common = require('./webpack.common.js');
var webpack = require('webpack');
module.exports = merge(common, {
plugins: [
new UglifyJSPlugin(),
new webpack.DefinePlugin({
'process.env': {NODE_ENV:"'production'"}
}),
],
})
(二) 后端模块源码示例
【路由示例】
login.js:
var express = require('express'); var path = require('path'); var router = express.Router(); /* 登录. */ router.get('/', function(req, res, next) { res.sendfile(path.join(__dirname, '../public/htmls/login.html')); }); module.exports = router;
(三) 前端模块源码示例
以login模块为例,
在测试文件index.html中,加入以下代码。
<div id="login"> <script type="text/javascript" src="login.dst.js"></script> </div>
login.dst.js由webpack生成。【app.js】import Vue from 'vue' import elementui from 'element-ui' import 'element-ui/lib/theme-default/index.css' import app from './app.vue' Vue.use(elementui); new Vue({ el:'#login', render: h => h(app) });
从app.vue中导入默认模块并命名为app,使用app模块将html中id为login的节点进行渲染。
【app.vue】
将大部分的前端代码封入app.vue文件中,通过vue-loader进行预编译。
(四) 使用静态资源
【css资源】
可在app.js中使用语句导入。如:
import 'element-ui/lib/theme-default/index.css'
也可以在app.vue中通过style节配置。如:
<style scope> .image { width: 100%; display: block; } a{text-decoration:none;} </style>
也可以在app.vue中引入。如:
<style src="./app.css"> </style>
或者
<style scope> @import "./app.css"; </style>
【文件资源】
文件资源,如图片。
可将文件放置在模块目录中,在app.vue的script节中用以下代码引用。
import TaskManager from "./TaskManager.jpg";
在webpack配置中,修改file-loader的配置:
{ test: /\.(png|jpe?g|gif|svg)(\?\S*)?$/, loader: 'file-loader', query: { // 目标路径是/images,生成文件时会在dst目录下建立images目录,将文件拷贝至后端public/images目录下即可 name: '/images/[name].[ext]?[hash]' } }
(五) 前后端通讯
Vue官方建议使用axios,搜了一下网上大部分都是对官网样例的翻译,从头开始教学的几乎没有,学习之后感觉可以分为以下几个步骤。
【配置】
唯一必须配置的是baseURL项,标明请求的资源地址。更改默认配置代码:
axios.defaults.baseURL = 'https://xxxx';
【使用】
可以实例化,也可以直接应用全局实例。
实例化代码:
var instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
全局应用:
axios.get('/tasks')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});
或者:
Vue.prototype.$http = axios;
可直接在method定义的函数中使用。
methods: {
postData () {
this.$http({
method: 'post',
url: '/task',
data: {
name: 'task001',
id: '001'
}
})
}
(六) 用户session管理
使用sessionStorage进行session管理,sessionStorage通过setItem( key, value)、getItem(key)、removeItem进行操作。
【登录记录session】
设置登录状态为用户ID。
// 验证用户...
sessionStorage.setItem('userID', userID);
this.$router.push({path:'/content'});
【登出时清除session】
sessionStorage.removeItem('userID');
【路由跳转时判断登录状态】
在app.js中,添加以下代码:
// 递归查找路由表routesArray中path为'routePath'的路由条目
var routePathInRoutes=function (routesArray, routePath) {
for(var i in routesArray){
if(routesArray[i].path == routePath){
return routesArray[i];
}
if(routesArray[i].children != null){
return routePathInRoutes(routesArray[i].children, routePath);
}
}
return null;
}
router.beforeEach(function (to, from, next) {
if(to.path == '/login') {
sessionStorage.removeItem('userID');
}
else{
var userID = sessionStorage.getItem('userID');
if((userID == null) || (userID == '')){
// 尚未登录,检查跳转路由是否需要认证
var route = routePathInRoutes(routes, to.path);
if (route !=null){
if(route.auth){
next({path:'/login'});
}
}
}
}
next();
})
(七) 使用vuex在前端保存状态信息
使用vuex在前端进行状态信息的管理。在前端设置目录store,其下设置文件index.js和session.js。
在app.js中引入vuex组件和index.js。
import Vuex from 'vuex'
import store from './store/index'
Vue.use(Vuex);
new Vue({
el:'#home',
router,
store,
render: h => h(app)
});
在index.js中定义各个组件,如session(登录状态的详细信息)、auth(认证相关信息)。
import Vue from 'vue'
import Vuex from 'vuex'
import session from "./session"
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production'
export default new Vuex.Store({
modules: {
session
},
strict: debug,
})
在session.js中定义具体的信息
/*
登录的用户信息
*/
const state={
// 当前登录的用户信息
user:{},
userID:'',
userName:'',
userPwd:'',
}
const getters={
userID: state => state.user.userID,
userName: state => state.user.userName,
userPwd: state => state.user.userPwd,
}
const mutations={
removeUser(state){
state.user={};
},
setUser(state, user) {
state.user=user;
},
}
export default{
state,
getters,
mutations
}
在登录时,更新信息。
this.$store.commit('setUser', {
userID: guid,
userName: name,
userPwd: pwd,
});
在注销时,清除信息。
this.$store.commit('removeUser');
三、 问题
(一) webpack run build报uglifyjs unexpected token错误
需要使用babel选项:"presets":["es2015"],可以在.babelrc文件中添加,也可以在package.json中添加。
(二) vue-router嵌套路由的展现问题
使用vue-router嵌套路由时,需要逐级展现。
【routers.js】
import demos from "./demos.vue"
import datagrid from"./datagrid.vue"
import demosabout from"./demosabout.vue"
{
path: '/demos',
component:demos,
name: 'demos',
icon: 'el-icon-more',
children:[
{
path:'/demos/demosabout',
component:demosabout,
name:'about',
icon: 'el-icon-share'
},
{
path:'/demos/datagrid',
component:datagrid,
name:'数据表格',
icon: 'el-icon-share'
}
]
export default routes;
首先在指向/home路由的页面中使用<router-view></router-view>标签进行渲染,此时将渲染出demons.vue中定义的组件,这时需要在demons.vue中再次使用<router-view></router-view>进行渲染,才可渲染出/demons/xxxx等组件。
【demons.vue】
<template>
<div>
<h1>{{title}}</h1>
<router-view></router-view>
</div>
</template>
<script>
export default {
data() {
return {
title: 'demos home page'
};
},
}
</script>
(三) 跨域访问问题
出现Access-Control-Allow-Origin错误,原因是默认情况下服务器都是不允许跨域访问的,如网站http://a.com访问http://b.com/records/,是不允许的,解决方法是在服务器后端设置允许跨域访问,即:在路由中设置。
router.get('/', function(req, res, next) {
// 允许跨域访问
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.json(mgr.allUsrs());
});
也可以通过app.use设定全局开放。
app.use('/api', function (req,res,next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Headers', 'Content-Type');
next();
})
注意:app.use()和app.all()的区别是use()将匹配以/api开头的所有路由,all()则仅匹配/api路由。
(四) axios异步访问无法访问vue实例的问题
axio是异步调用的,在其.then函数作用域中是无法访问vue实例的this引用的,解决方法很简单,只需在then的函数体上绑定this即可。代码如下:
axios.get('/tasks')
.then(function (response) {
for(var i in response.data)
{
var t = new Object();
t['guid'] = response.data[i]['guid'];
t['name'] = response.data[i]['name'];
t['remark'] = response.data[i]['remark'];
this.tasks.push(t);
}
}.bind(this))
.catch(function (error) {
console.log(error);
});
(五) vue报template compiler is not available的问题
在vue实例中应用template模板语法,报以下错误。
debug.dst.js:6[Vue warn]: You are using the runtime-only build of Vue where the templatecompiler is not available. Either pre-compile the templates into renderfunctions, or use the compiler-included build.
这个在vue/dist/README.md中描述的比较清楚了,Compiler版本、Runtime版本,简单说一下区别,看例子。
new Vue({ el:'#debug', template:'<div><h1>{{a}}</h1></div>', data:{ a:'aaaaaa', } });
像上述例子中需要对template字符串进行模板渲染(或者将绑定的html对象作为模板进行渲染),则必须使用Compiler版本。
而使用vue-loader加载.vue文件的情况,webppack在打包时已经对模板进行了渲染,因此仅需Runtime版本即可。
使用Compiler版本的方法在README.md中也已给出,就是在打包配置文件中加入精确匹配解析项,以webpack为例。
module.exports = {
// ...
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
}
}
}
Vue推荐使用runtime版本。
四、 引用
https://vue-loader.vuejs.org/zh-cn/。
https://doc.webpack-china.org。
http://www.cnblogs.com/tugenhua0707/p/4793265.html。
https://github.com/jantimon/html-webpack-plugin。
https://www.npmjs.com/package/axios。