当前位置:首页 » 《随便一记》 » 正文

教你搭建一个页面秒开的Vue项目_黄河爱浪

25 人参与  2021年12月28日 10:01  分类 : 《随便一记》  评论

点击全文阅读


操作系统:windows 10
Node:14.7.0
npm版本:6.14.7
vue-cli:4.5.13

前言

该项目是一个【移动端】的 web 项目,使用 Vue.js 的原因有以下几点:

  1. hash 路由的特性可以解决各浏览器因为页面的返回不刷新的问题;
  2. 拥抱工程化前端项目开发环境;
  3. 提升自我 及 扩展公司 web 组的技术栈;

项目中代码部分会在文章中后段具体说明,请大家耐心阅读全文。

一、Vue.js 版本的选择

考虑到整体的技术水平、兼容性要求和生态建设程度(说人话就是某度能解决多少问题)选择了 Vue 2.x。

二、CSS扩展语言的选择

选择 Sass 的原因是其本身上手简单外,因各大框架平台也是默认 Sass 为CSS的扩展语言。所以不需要太多的理由跟着大厂走总没错。

三、UI框架的选择

UI框架能提升开发效率,主要使用 日历、滚动选择、弹窗、吐丝提示 等常用组件,最终根据大家的喜爱程度选择了 Vant UI。

四、如何提升项目打开速度

在 web 中,页面打开速度是考量一个项目好坏非常重要的指标,因此在项目开始开发之前需要着重解决的问题(在主文件 main.js 中引用大量的依赖是一个非常忌讳的事情)。下面是我为减少打包后文件过大的几个要点:

  1. Vue、vue-router、vuex 这些不可或缺的依赖文件采用 排除合并打包 的方式;
  2. 项目默认的配置是会将 小于 10k 的图片转为 base64 编码的方式,当图片过多的时候会导致文件非常大,则调整 5k 以上的文件都不转为 base64 编码 ;
  3. 路由懒加载(常见的优化方式);
  4. 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,本文原创,著作权归作者所有,转载请注明原链接及出处


点击全文阅读


本文链接:http://zhangshiyu.com/post/32242.html

文件  路由  项目  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1