操作系统:windows 10
Node:14.7.0
npm版本:6.14.7
vue-cli:4.5.13
前言
该项目是一个【移动端】的 web 项目,使用 Vue.js 的原因有以下几点:
- hash 路由的特性可以解决各浏览器因为页面的返回不刷新的问题;
- 拥抱工程化前端项目开发环境;
- 提升自我 及 扩展公司 web 组的技术栈;
项目中代码部分会在文章中后段具体说明,请大家耐心阅读全文。
一、Vue.js 版本的选择
考虑到整体的技术水平、兼容性要求和生态建设程度(说人话就是某度能解决多少问题)选择了 Vue 2.x。
二、CSS扩展语言的选择
选择 Sass 的原因是其本身上手简单外,因各大框架平台也是默认 Sass 为CSS的扩展语言。所以不需要太多的理由跟着大厂走总没错。
三、UI框架的选择
UI框架能提升开发效率,主要使用 日历、滚动选择、弹窗、吐丝提示 等常用组件,最终根据大家的喜爱程度选择了 Vant UI。
四、如何提升项目打开速度
在 web 中,页面打开速度是考量一个项目好坏非常重要的指标,因此在项目开始开发之前需要着重解决的问题(在主文件 main.js 中引用大量的依赖是一个非常忌讳的事情)。下面是我为减少打包后文件过大的几个要点:
- Vue、vue-router、vuex 这些不可或缺的依赖文件采用 排除合并打包 的方式;
- 项目默认的配置是会将 小于 10k 的图片转为 base64 编码的方式,当图片过多的时候会导致文件非常大,则调整 5k 以上的文件都不转为 base64 编码 ;
- 路由懒加载(常见的优化方式);
- UI框架组件按需加载(这是必须的);
五、项目目录结构规划
项目根目录
├── public // 公共依赖
├── src // 项目文件
│ ├── assets // ├── 静态资源
│ ├── components // ├── 通用组件
│ ├── pages // ├── 业务页面
│ ├── request // ├── 请求axios配置
│ ├── router // ├── 路由配置
│ ├── store // ├── 全局状态管理Vuex
│ ├── main.js // ├── 项目入口文件
├── vue.config.js // 项目配置文件
└── README.md // 项目自述文件
六、项目源码示例及说明:
1. Vue、vue-router、vuex 文件排除合并打包
a) public/index.html 文件 修改文件的引用方式
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<!-- head部分代码省略 -->
</head>
<body>
<div id="app"></div>
<% if(process.env.NODE_ENV === 'development'){ %>
<!-- 开发环境引用未压缩的文件,方便使用 Vue.js devtools 工具调试-->
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.js"></script>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.5.1/vue-router.js"></script>
<script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vuex/3.6.2/vuex.js"></script>
<% }else{ %>
<!-- 非开发环境引用压缩的文件,提高加载速度 -->
<script type="text/javascript" src="<%= BASE_URL %>js/vue.2.6.11.min.js"></script>
<script type="text/javascript" src="<%= BASE_URL %>js/vue-router.3.5.1.min.js"></script>
<script type="text/javascript" src="<%= BASE_URL %>js/vuex.3.6.2.min.js"></script>
<% } %>
</body>
</html>
b) /vue.config.js 文件 设置排除不打包文件
module.exports = {
// ...
configureWebpack: {
/* 排除不打包的文件,解决主文件过大的问题 */
externals: {
'vue': 'Vue',
'vue-router':'VueRouter',
'vuex':'Vuex'
}
},
// ...
}
2. /vue.config.js 文件 设置5K以上的图片文件不参与打包
module.exports = {
// ...
/* 调整内联文件的大小限制,让小图片不转为base64 */
chainWebpack: config => {
config.module
.rule('images')
.use('url-loader')
.loader('url-loader')
.tap(options => Object.assign(options, { limit: 5120 }))
},
// ...
}
3. Vant UI 设置按需引入
文档:传送门
// /babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
}
4. 项目入口文件源码
a) /src/main.js
import Vue from 'vue'
import App from './App.vue'
// router
import router from './router/index'
// vuex
import store from './store/index'
// babel ES6编译
import 'babel-polyfill';
// 自定义全局方法 iGlobal
import iGlobal from './assets/js/iGlobal'
Vue.use(iGlobal);
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
b) /src/App.vue
<template>
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
</template>
<script>
import { Toast } from "vant";
export default {
name: "App",
computed:{
},
components: {
},
created(){
// 提示弹窗项目中使用频率非常高,挂载在 window 对象,方便各 JS 文件中调用
window.Toast = Toast;
},
mounted(){
}
};
</script>
<style lang='scss'>
// flex 布局盒子
@import '@/assets/css/helang-flex.scss';
//公共样式
@import '@/assets/css/mobile.scss';
</style>
5. 路由文件源码
a) /src/router/index.js 路由入口文件
// 配置路由相关的信息
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1.通过Vue.use(插件), 安装插件
Vue.use(VueRouter);
// 导航路由
import HomeRouter from './pages_home'
// 首页路由
import IndexRouter from './pages_index'
// 异常路由
import ErrorRouter from './pages_error'
const routes = [
...HomeRouter,
...IndexRouter,
...ErrorRouter
]
// 2.创建VueRouter对象
const router = new VueRouter({
mode:'hash',
// 配置路由和组件之间的应用关系
routes,
// 使用前端路由,当切换到新路由时,想要页面滚到顶部
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
})
// 路由全局前置守卫,需登录页面优先拦截跳转至登录
router.beforeEach((to, from, next) => {
// 设置路由权限
if(/^\/(home|user|index)/.test(to.path)){
if(window.iGlobal.isLogin()){
next();
}else{
next({
path:'/login',
replace: true
})
}
}else{
next();
}
})
// 路由全局后置钩子,设置标题
router.afterEach((to) => {
// 设置标题
document.title = to.meta.title || 'Vue 项目实战';
})
// 3.抛出 router 对象
export default router
b) 页面路由文件示例
let pagesRouter = [
{
path: '*', // 页面未找到
component: ()=>import('@/pages/error/404.vue'),
meta: {
title:'页面未找到'
}
}
]
export default pagesRouter
6. /src/store/index.js vuex 源码示例
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
// ...
},
mutations: {
// ...
}
})
export default store
7. request 请求 axios.js
a) /src/request/index.js 请求入口文件源码
// 配置路由相关的信息
import axios from 'axios'
// 序列化请求
import Qs from 'qs'
const service = axios.create({
//...
timeout: 5000 // 请求超时时长
})
// 公共参数
const publicParams = {
// ...
terminal:'web'
}
// 添加请求拦截器
service.interceptors.request.use(function (request) {
if(/^post$/i.test(request.method)){
let data = {};
if(request.data){
data = request.data;
}
// POST 请求将 公共参数添加到请求 data 对象中
request.data = {...publicParams,...data};
}else if(/^get$/i.test(request.method)){
let data = {};
if(request.params){
data = request.params;
}
// GET 请求将 公共参数添加到请求 params 对象中
request.params = {...publicParams,...data};
}
// 在发送请求之前做些什么
// request.headers['Content-Type'] = 'application/x-www-form-urlencoded';
// POST请求需要序列化为字符串
if(/^post$/i.test(request.method)){
request.data = Qs.stringify(request.data);
}
// 添加用户签名信息,可自行修改
let access = window.iGlobal.getAccess(request.url);
request.headers = {...access,...request.headers}
return request;
}, function (error) {
// 对请求错误做些什么,抛出错误
return Promise.reject(error);
});
// 添加响应拦截器
service.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response.data;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
/* axios 请求库全局配置 */
service.defaults.headers.common['Content-Type'] = 'application/x-www-form-urlencoded';
export default service;
b) /src/request/api/login.js 具体业务接口文件源码
import request from '../index'
// 用户/手机号密码登陆
let login = (params)=>{
return request.post('/api/login',params)
}
export default {
login,
}
c) 页面使用 axios 请求源码示例
<script>
// 引用 登录请求接口文件
import loginApi from '@/request/api/login'
export default {
name: "userLogin",
data() {
return {
// ...
};
},
mounted() {
},
methods: {
login:function(){
// ...
loginApi.login({
// ... 登录参数
}).then((res)=>{
// ... 登录成功
}).catch((err)=>{
// ... 登录失败
})
}
}
};
</script>
项目中未将 axios.js 在 main.js 中引用是为降低打包后的 chunk-vendors-*.js 入口文件 大小(实测能降低30KB左右),提高首页加载速度。
8. 项目通用方法 iGlobal.js 源码
let iGlobal = {
// 获取授权
getAccess(uri){
// ...
},
/*
获取环境
dev:开发,test:测试,release:生产
*/
getENV(){
// ...
},
// 获取用户信息
getUserInfo(keyName = undefined){
// ...
},
// 获取用户令牌
getUserToken(){
// ...
},
// 是否登录
isLogin(){
let user = this.getUserInfo();
return !!user;
},
// 运行环境
environment(){
let environment = 'browser'
let _UA = window.navigator.userAgent;
if(/QQ\/\d/i.test(_UA)){
environment = 'mqq'; // 手机QQ
}else if(/micromessenger/i.test(_UA.match(/MicroMessenger/i))){
environment = 'wechat'; // 微信
}
return environment;
},
// 设备标识
terminal(){
let _UA = window.navigator.userAgent;
let terminal = '';
if (/Android|BlackBerry/i.test(_UA)) {
terminal = 'android';
} else if (/webOS|iPhone|iPad/i.test(_UA)) {
terminal = 'ios';
} else if (/macintosh|mac os x/i.test(_UA)) {
terminal = 'mac';
} else {
terminal = 'windows';
}
return terminal;
},
/**
* 添加js脚本文件
* url:文件地址,name:JS的变量名,比如 jQuery 是 $。
* 该方法很重要,为了动态添加一些第三方SDK文件使用
*/
appendScript(url = undefined,name = undefined){
// ...
// 请查看文章结尾 [附录] 源码
},
/** 常用正则 */
regExps:{
email: /^[0-9a-zA-Z_]+@[0-9a-zA-Z_]+[.]{1}[0-9a-zA-Z]+[.]?[0-9a-zA-Z]+$/, //邮箱
mobile: /^(?:1\d{2})-?\d{5}(\d{3}|\*{3})$/, //手机号码
qq: /^[1-9][0-9]{4,9}$/, //QQ
befitName: /^[a-z0-9A-Z\u4e00-\u9fa5]+$/, //合适的用户名,中文,字母,数字
befitPwd: /^[a-z0-9A-Z_]+$/, //合适的密码,字母,数字,下划线
allNumber: /^[0-9]+.?[0-9]$/ //全部为数字
},
showLoading(message = '提交中'){
window.Toast.loading({
message: message,
forbidClick: true,
duration: 0,
});
}
}
/**
把 iGlobal 挂载到 Vue.prototype 中,属性名为 iGlobal。同时挂载到 window 浏览器全局对象,方便非 vue 实例方法调用
页面 Vue 文件中调用方式 this.iGlobal.*
非 页面 Vue 文件调用方式 window.iGlobal.*
*/
let install = (Vue) => {
window.iGlobal = Vue.prototype.iGlobal = iGlobal;
}
// 导出 install 方法,可使用 Vue.use() 这种装逼方式挂载
export default install;
9. 项目rem计算&全局flex 布局文件
- rem计算:https://mydarling.gitee.io/resource/files/helang-flexible.js
- flex布局:https://mydarling.gitee.io/resource/files/helang-flex.scss
10. 浏览器兼容性
// /package.json
{
//...
"browserslist": [
"last 4 version",
"IE 10"
]
}
附录
1. appendScript 方法源码
appendScript(url = undefined,name = undefined){
return new Promise((resolve, reject)=>{
if(name && name in window){
resolve({
code:2,
msg:'文件已被引用'
});
return;
}
if(!url){
reject({
code:0,
msg:'无效的文件地址'
});
return;
}
let jsEl = document.createElement('script');
jsEl.type = 'text/javascript';
jsEl.src = url;
document.querySelector("#files-container").appendChild(jsEl);
jsEl.onload = ()=>{
resolve({
code:1,
msg:'文件加载成功'
});
}
jsEl.onerror = ()=>{
reject({
code:0,
msg:'文件加载失败'
});
}
})
}
// 方法使用示例
export default {
// ...
mounted(){
this.iGlobal.appendScript('/js/jQuery.min.js,'jQuery').then(res=>{
if(res.code > 0){
// jQuery 相关代码
// $("#app").html()
}
});
}
// ...
}
作者:黄河爱浪 QQ:1846492969,邮箱:helang.love@qq.com
公众号:
web-7258
,本文原创,著作权归作者所有,转载请注明原链接及出处