此章节全力讲解前端基本项目架构,通过此章节可搭建一个通用性的前端架构,内容涵盖跨域方案、路由封装、错误拦截等。
同域名
、同端口
、同协议
CORS跨域 - 服务端设置,前端直接调用
说明:后台允许前端某个站点进行访问。如Easy Mock:开源的公共MOCK平台,公共接口。
控制台查看network信息
响应头中:
Access-Control-Allow-Credentials
:允许前端将cookie带过去
Access-Control-Allow-Origin
:表示允许指定的这个网址访问mock的接口
前端适配,后台配合
说明:需要前后端同时改造
安装jsonp
安装并添加到环境中
npm i jsonp --save-dev
jsonp不是请求在network中的XHR里面没有,JS里面有,是js的一段脚本。
let url2 = 'https://order.imooc.com/pay/cartorder'
axios.get(url2).then((data)=>{
console.log(data)
})
jsonp(url2,{},(err,res)=>{
console.log(res)
})
当使用axios时控制台输出信息,表示不是允许的网站,无Access-Control-Allow-Origin请求头。
实现:通过修改nginx服务器配置来实现;
说明:前端修改,后台不动;
具体操作:
1.在整个项目项目中新增vue.config.js的配置文件
2.文件中写入:
// 开发环境下的接口代理 访问/a代理到/b,则实际访问的/b
// webpack的配置表传送给nodejs服务器
// nodejs遵循commonjs规范抛出就不用import了
module.exports = {
// 自动加载devServer中的配置表
devServer:{
host:'localhost',
port:8080,
// 代理
proxy:{
// 访问/pay时实现拦截转发到target中
'/api':{
// 目标网址,内部访问到慕课网的接口
target:'https://order.imooc.com',
changeOrigin:true,
pathRewrite:{
'/api':''
}
}
}
}
}
实现原理:
书写好config.js后重启项目,vue会自动加载此配置文件下的devServer配置表
针对proxy中做路由统一拦截(此处是统一拦截的/api),在.vue文件中定义url时都统一定义为如下:let url2 = ‘/api/pay/cartorder’,在拦截后进行访问目标地址时由**changeOrigin:true,pathRewrite:{’/api’:’ '}**自动去掉/api进行访问。
此时看似访问的localhost:8080/api/pay/cartorder实则访问的https://order.imooc.com/pay/cartorder
初始化VScode内容:删除components文件夹下的最初的HelloWord组件,以及删除App.vue下对HelloWord组件的所有引用,并增加router-view的入口。
在src目录下新建api文件夹统一管理路由api
src下创建util文件夹存放公共方法,如格式化,数组转换之类的方法等。
src下创建storage文件夹存放对数据存值取值等的工具箱。
src下创建store文件夹存放vuex的内容。
src下创建router.js存放路由
src下创建pages文件夹存放页面文件
pages创建index.vue(首页)、home.vue(主结构、存放公共的头部和尾部),product.vue(产品栈)、detail.vue(商品详情)、orderList.vue(订单页)、order.vue(订单主结构)、orderConfirm.vue(订单确认)、cart.vue(购物车)、login.vue(登录)、orderPay.vue(支付)、alipay.vue(支付跳转中间页)
懒加载:vue-lazyload
ui库:element-ui
sass编译:node-sass sass-loader
轮播:vue-awesome-swiper
axios:vue-axios
cookie:vue-cookie
npm i vue-lazyload element-ui node-sass sass-loader vue-awesome-swiper vue-axios vue-cookie --save-dev
由页面划分路由,找共性,拆分父子组件。
在router.js中封装所有的路由,并重定向到index
import Vue from 'vue'
import router from 'vue-router'
import Home from './pages/home.vue'
import Index from './pages/index.vue'
import Product from './pages/product.vue'
import Detail from './pages/detail.vue'
import Cart from './pages/cart.vue'
import Order from './pages/order.vue'
import OrderList from './pages/orderList.vue'
import OrderConfirm from './pages/orderConfirm.vue'
import OrderPay from './pages/orderPay.vue'
import AliPay from './pages/alipay.vue'
Vue.use(router);
export default new router({
routes:[
{
path:'/',
name:'home',
component:Home,
redirect:'/index',
children:[
{
path:'/index',
name:'index',
component:Index
},
{
path:'/product/:id',
name:'product',
component:Product
},
{
path:'/detail/:id',
name:'detail',
component:Detail
}
]
},
{
path:'/cart',
name:'cart',
component:Cart
},
{
path:'/order',
name:'order',
component:Order,
children:[
{
path:'list',
name:'order-list',
component:OrderList
},
{
path:'confirm',
name:'order-confirm',
component:OrderConfirm
},
{
path:'pay',
name:'order-pay',
component:OrderPay
},
{
path:'alipay',
name:'alipay',
component:AliPay
}
]
}
]
})
在main.js中引入,并全局使用
import Vue from 'vue'
import router from './router'
import App from './App.vue'
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App),
}).$mount('#app')
定义index是home的子页面,在home中写入router-view用来显示子页面内容:
访问路由:http://localhost:8080/#/index
页面显示为:绿色框为公共组件,黑色为视图层,home相当于整个容器。
访问路由:http://localhost:8080/#/order/list
大小:cookie 4k,storage 5m
有效期:cookie拥有有效期可以通过expires设置失效时间,不设置默认关闭浏览器即失效,localStorage永久存储需要手动清除,sessionStorage会话存储,关闭网页就清除了信息。
http请求:cookie会携带在http头中,发送到服务器端,如果使用cookie保存过多数据会带来性能问题,存储在内存中,Storage只存储在浏览器端不参与和服务器的通信。
路径:Cookie有路径限制,Storage只存储在域名下。
API:Cookie没有特定的API,Storage有对应的API
设置key,value
sessionStorage.setItem(“key”, “value”);
localStorage.setItem(“site”, “js8.in”);
通过key获取value
var value = sessionStorage.getItem(“key”);
var site = localStorage.getItem(“site”);
删除对应key
sessionStorage.removeItem(“key”); localStorage.removeItem(“site”);
清除所有的key/value
sessionStorage.clear();
localStorage.clear();
在storage文件夹下的index.js中,抛出四个操控Storage的函数,存储值——setItem,获取值——getItem,获取整个浏览器的缓存信息——getStorage,清空某一个值——clear
函数中为:
getStorage(){
return JSON.parse(window.sessionStorage.getItem(STORAGR_KEY) || '{}');
}
//storage/index.js (第二种情况返回的值:'data','this.getStorage()['data']')
setItem(key,value){
//此时的val为sessionStorage的JSON格式
let val = this.getStorage();
//往大的模块中覆盖旧的值 this.getStorage()['data']=this.getStorage()['data']
val[key] = value;
//存完之后转换为字符串写入Storage信息中覆盖原来的Storage_key
window.sessionStorage.setItem(Storage_Key,JSON.stringify(val));
},
//App.vue中导入
import storage from './storage/index'
//App.vue中mounted中使用
storage.setItem('id','001')
storage.setItem('data',{})
val[key]相当于JSON.parse(window.sessionStorage.getItem(‘mail’))[‘id’] //打印001
在已有的模块中再添加内容,如往data中添加username,以及userblogs(包含blog1,blog2)
//App.vue
//参数:key,value,添加到哪里的模块
storage.setItem('username','DDDZ','data')
storage.setItem('userblogs',{
'blog1':'name1',
'blog2':'name2'
},'data')
//storage/index.js
setItem(key,value,module_name){
//拿到data下面的所有值引入getItem第一种情况返回为this.getStorage()['data']
let val = this.getItem(module_name);
//等同于this.getStorage()['data']['username']='DDDZ'
val[key] = value;
//module_name成为key,val为value执行第一种情况 ('data','this.getStorage()['data']')
this.setItem(module_name,val);
},
getItem(key){
return this.getStorage()[key]
}
//如传参:('username','data')
getItem(key,module_name){
//递归获取 module_name变成key this.getItem('data');到第一种情况返回this.getStorage()['data'] 为val
let val = this.getItem(module_name);
//return this.getStorage()['data']['username']
if(val) return val[key];
}
//storage/index.js
clear(key){
//获取
let val = this.getStorage()
//删除 this.getStorage()['id']
delete val[key]
//更新
window.sessionStorage.setItem(Storage_Key, JSON.stringify(val))
}
第二种:
//storage/index.js
clear(key,module_name){
//获取
let val = this.getStorage()
//this.getStorage()['data'] 判断如果没有值,返回
if (!val[module_name]) return;
//this.getStorage()['data']['username'] 删除
delete val[module_name][key];
//更新
window.sessionStorage.setItem(Storage_Key, JSON.stringify(val))
}
//App.vue
storage.clear('id')
storage.clear('username','data')
//storage/index.js
//Storage封装 如:value为{"user":{"username":"jack","age":30,"sex":1}}
const Storage_Key = 'mail';
export default {
//module_name是某个模块如user
//存储值
//storage/index.js (第二种情况返回的值:'data','this.getStorage()['data']')
setItem(key, value, module_name) {
//获取某个模块下面的属性user下面的username
if (module_name) {
//拿到data下面的所有值引入getItem this.getStorage()['data']
let val = this.getItem(module_name);
//this.getStorage()['data']['username']='DDDZ'
//val[key]相当于JSON.parse(window.sessionStorage.getItem('mail'))['id'] //打印001
val[key] = value;
//module_name成为key,val为value执行第一种情况 ('data','this.getStorage()['data']')
this.setItem(module_name, val);
}
else {
//此时的val为sessionStorage的JSON格式
let val = this.getStorage();
//往大的模块中覆盖旧的值 this.getStorage()['data']=this.getStorage()['data']
val[key] = value;
//存完之后转换为字符串写入Storage信息中覆盖原来的Storage_key
window.sessionStorage.setItem(Storage_Key, JSON.stringify(val));
}
},
getItem(key, module_name) {
if (module_name) {
//递归获取 module_name变成key this.getItem('data');到第一种情况返回this.getStorage()['data'] 为val
let val = this.getItem(module_name);
//return this.getStorage()['data']['username']
if (val) return val[key];
}
//获取的user相当于(JSON.parse(window.sessionStorage.getItem("mall") || '{}'))["user"]
return this.getStorage()[key];
},
//获取整个缓存信息
getStorage() {
return JSON.parse(window.sessionStorage.getItem(Storage_Key) || '{}');
},
clear(key, module_name) {
//获取
let val = this.getStorage();
if (module_name) {
//this.getStorage()['data'] 判断如果没有值,返回
if (!val[module_name]) return;
//this.getStorage()['data']['username'] 删除
delete val[module_name][key];
} else {
//删除 this.getStorage()['id']
delete val[key];
}
//更新
window.sessionStorage.setItem(Storage_Key, JSON.stringify(val));
}
}
统一报错、未登录统一拦截、请求值,返回值统一处理。
// 添加响应拦截器(返回值拦截)
axios.interceptors.response.use((response)=>{
//获取所有的接口数据
let res = response.data;
//检测已经登陆,返回数据
if(res.status == 0){
//返回接口里面data包含的值
return res.data;
}
//检测未登录,拦截跳转到登录页
else if(res.status == 10){
window.location.href = '/#/login';
}
//登录失败,错误的提示信息
else{
alert(res.msg);
}
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
}
)
使用JSONP或CORS跨域以及接口代理时的baseURL
设置请求默认地址baseURL和响应超时时间
baseURL根据前端的跨域方式做调整:
JSONP或CORS跨域:后端域名和前端不一样时用baseURL用’http://完整的url地址‘ 。
接口代理:后端与前端一样时,前端在这里定义了/api后端接口也要统一为/api,后端没有/api的话可以前端在转发时统一去掉/api。拦截时接口代理中用:changeOrigin:true,pathRewrite:{’/api’:’’}去掉/api。
//接口代理
axios.defaults.baseURL = '/api';
//CORS或JSONP跨域
axios.defaults.baseURL = 'https://order.imooc.com/pay/cartorder'
//设置超时时间
axios.defaults.timeout = 8000;
为什么要去设置?
"scripts": {
"serve": "vue-cli-service serve --mode=development",
//自定义的环境变量prev
"prev":"vue-cli-service serve --mode=prev",
"build": "vue-cli-service build --mode=production",
"lint": "vue-cli-service lint"
},
let baseUrl;
//根据不同的环境输出不同的url地址
//在package.json中增加 --mode=XXX参数(环境变量传给参数)
//process.env取到当前nodejs服务器下的环境变量
switch (process.env.NODE_ENV) {
//开发development,测试test,生产production
case 'development':
baseUrl = ' http://dev-mall-pre.springboot.cn/api'
break;
case 'test':
baseUrl = 'http://test-mall-pre.springboot.cn/api'
break;
case 'production':
baseUrl = 'http://mall-pre.springboot.cn/api'
break;
//自定义环境变量prev一定要在加一个文件.env.prev,并且在里面配置
case 'prev':
baseUrl = 'http://prev-mall-pre.springboot.cn/api'
break;
default:
baseUrl = 'http://dev-mall-pre.springboot.cn/api'
break;
}
export default{
baseUrl
}
import env from './env'
axios.defaults.baseURL = env.baseUrl;
public文件夹下创建mock文件夹,创建一个user文件夹存放login.json文件作为mock。
在app.vue中发起axios请求,在main.js中注释掉baseURL
easy-mock官网
//App.vue
this.axios.get('/user/login').then((res)=>{
this.res = res
})
//main.js
axios.defaults.baseURL = '换成自己在easy-mock的baseURL地址';
//安装mockjs
npm i mockjs --S
//main.js
//mockjs 开关,设置按需加载
const mock = true
if(mock){
require('./mock/api') //加载mock文件
}
axios.defaults.baseURL = '/api';
//App.vue
this.axios.get('/user/login').then((res)=>{
this.res = res
})
接口代理:
在vue.config.js中其实也是一种拦截,识别到url中的/api后拦截转发到真实的接口地址获取数据。
接口拦截:
用axios,在main.js中添加baseURL统一接口地址的起始头为/api,添加interceptors拦截器对接口返回的数据进行判定处理,如识别到未登录、已登录、登录错误等。
mock.js:
获取本地模拟数据,使用jsonp时mockjs拦截不到,访问接口返回在Network中的JS里。使用axios时,mock 开关,关闭拦截,则没有被拦截,访问接口返回真实接口数据,返回信息在Network的XHR中。mock 开关,打开拦截则URL被拦截访问本地数据。
举例:
//App.vue
let url2 = '/pay/cartorder'
this.axios.get(url2).then((res)=>{
this.res2 = res
console.log(res)
})
//main.js
axios.defaults.baseURL = '/api';
//mock开关打开,axios发url请求时被拦截去访问./mock/api文件里面的/api/pay/cartorder下的模拟接口数据而不会去访问vue.config.js内的真实接口地址
const mock = true
if(mock){
require('./mock/api')
}
//vue.config.js
module.exports = {
// 自动加载devServer中的配置表
devServer:{
host:'localhost',
port:8080,
// 代理
proxy:{
// 访问/pay时实现拦截转发到target中
'/api':{
// 目标网址,内部访问到慕课网的接口
target:'https://order.imooc.com',
changeOrigin:true,
pathRewrite:{
'/api':''
}
}
}
}
}