搭建一個vue-cli4+webpack移動端框架

轉載:https://juejin.im/post/5eb766296fb9a0432f0ff8c7

簡介

這是基于 vue-cli4 實現的移動端框架,其中包含項目常用的配置,組件封裝及webpack優化方法,可供快速開發使用。

技術棧:vue-cli4 + webpack4 + vant + axios + less + postcss-px2rem

源碼 github.com/Michael-lzg…

// 安裝依賴
npm install
 
// 本地啟動
npm run dev
 
// 生產打包
npm run build
復制代碼
  • vue-cli4腳手架
  • vant按需引入
  • 移動端rem適配
  • axios攔截封裝
  • util工具類函數封裝
  • vue-router配置
  • 登錄權限校驗
  • 多環境變量配置
  • vue.config.js配置
  • toast組件封裝
  • dialog組件封裝
  • 跨域代理設置
  • webpack打包可視化分析
  • CDN資源優化
  • gzip打包優化
  • 首頁添加骨架屏

關于更多的webpack優化方法,可參考 github.com/Michael-lzg…

配置 vant

vant 是一套輕量、可靠的移動端 Vue 組件庫,非常適合基于 vue 技術棧的移動端開發。在過去很長的一段時間內,本人用的移動端 UI 框架都是 vux。后來由于 vux 不支持 vue-cli3,就轉用了 vant,不得不說,無論是在交互體驗上,還是代碼邏輯上,vant 都比 vux 好很多,而且 vant 的坑比較少。

對于第三方 UI 組件,如果是全部引入的話,比如會造成打包體積過大,加載首頁白屏時間過長的問題,所以按需加載非常必要。vant 也提供了按需加載的方法。babel-plugin-import 是一款 babel 插件,它會在編譯過程中將 import 的寫法自動轉換為按需引入的方式。

1、安裝依賴

npm i babel-plugin-import -D

2、配置 .babelrc 或者 babel.config.js 文件

// 在.babelrc 中添加配置
{
  "plugins": [
    ["import", {
      "libraryName": "vant",
      "libraryDirectory": "es",
      "style": true
    }]
  ]
}
 
// 對于使用 babel7 的用戶,可以在 babel.config.js 中配置
module.exports = {
  plugins: [
    ['import', {
      libraryName: 'vant',
      libraryDirectory: 'es',
      style: true
    }, 'vant']
  ]
};

3、按需引入

你可以在代碼中直接引入 Vant 組件,插件會自動將代碼轉化為方式二中的按需引入形式

import Vue from 'vue'
import { Button } from 'vant'
 
Vue.use(Button)

rem 適配

移動端適配是開發過程中不得不面對的事情。在此,我們使用 postcss 中的 px2rem-loader,將我們項目中的 px 按一定比例轉化 rem,這樣我們就可以對著藍湖上的標注寫 px 了。

我們將 html 字跟字體設置為 100px,很多人選擇設置為 375px,但是我覺得這樣換算出來的 rem 不夠精確,而且我們在控制臺上調試代碼的時候無法很快地口算得出它本來的 px 值。如果設置 1rem=100px,這樣我們看到的 0.16rem,0.3rem 就很快得算出原來是 16px,30px 了。

具體步驟如下;

1、安裝依賴

npm install px2rem-loader --save-dev

2、在 vue.config.js 進行如下配置

  css: {
    // css預設器配置項
    loaderOptions: {
      postcss: {
        plugins: [
          require('postcss-px2rem')({
            remUnit: 100
          })
        ]
      }
    }
  },
復制代碼

3、在 main.js 設置 html 跟字體大小

function initRem() {
  let cale = window.screen.availWidth > 750 ? 2 : window.screen.availWidth / 375
  window.document.documentElement.style.fontSize = `${100 * cale}px`
}
 
window.addEventListener('resize', function() {
  initRem()
})
復制代碼

axios 請求封裝

1、設置請求攔截和響應攔截

const PRODUCT_URL = 'https://xxxx.com'
const MOCK_URL = 'http://xxxx.com'
let http = axios.create({
  baseURL: process.env.NODE_ENV === 'production' ? PRODUCT_URL : MOCK_URL,
})
// 請求攔截器
http.interceptors.request.use(
  (config) => {
    // 設置token,Content-Type
    var token = sessionStorage.getItem('token')
    config.headers['token'] = token
    config.headers['Content-Type'] = 'application/json;charset=UTF-8'
    // 請求顯示loading效果
    if (config.loading === true) {
      vm.$loading.show()
    }
    return config
  },
  (error) => {
    vm.$loading.hide()
    return Promise.reject(error)
  }
)
// 響應攔截器
http.interceptors.response.use(
  (res) => {
    vm.$loading.hide()
    // token失效,重新登錄
    if (res.data.code === 401) {
      //  重新登錄
    }
    return res
  },
  (error) => {
    vm.$loading.hide()
    return Promise.reject(error)
  }
)
復制代碼

2、封裝 get 和 post 請求方法

function get(url, data, lodaing) {
  return new Promise((resolve, reject) => {
    http
      .get(url)
      .then(
        (response) => {
          resolve(response)
        },
        (err) => {
          reject(err)
        }
      )
      .catch((error) => {
        reject(error)
      })
  })
}
 
function post(url, data, loading) {
  return new Promise((resolve, reject) => {
    http
      .post(url, data, { loading: loading })
      .then(
        (response) => {
          resolve(response)
        },
        (err) => {
          reject(err)
        }
      )
      .catch((error) => {
        reject(error)
      })
  })
}
 
export { get, post }
復制代碼

3、把 get,post 方法掛載到 vue 實例上。

// main.js
import { get, post } from './js/ajax'
Vue.prototype.$http = { get, post }
復制代碼

工具類函數封裝

1、添加方法到 vue 實例的原型鏈上

export default {
  install (Vue, options) {
    Vue.prototype.util = {
      method1(val) {
        ...
      },
      method2 (val) {
       ...
      },
  }
}
復制代碼

2、在 main.js 通過 vue.use()注冊

import utils from './js/utils'
Vue.use(utils)
復制代碼

vue-router 配置

平時很多人對 vue-router 的配置可配置了 path 和 component,實現了路由跳轉即可。其實 vue-router 可做的事情還有很多,比如

路由懶加載配置
改變單頁面應用的 title
登錄權限校驗
頁面緩存配置
路由懶加載配置

Vue 項目中實現路由按需加載(路由懶加載)的 3 中方式:

// 1、Vue異步組件技術:
{
  path: '/home',
  name: 'Home',
  component: resolve => reqire(['../views/Home.vue'], resolve)
}
 
// 2、es6提案的import()
{
  path: '/',
  name: 'home',
  component: () => import('../views/Home.vue')
}
 
// 3、webpack提供的require.ensure()
{
  path: '/home',
  name: 'Home',
  component: r => require.ensure([],() =>  r(require('../views/Home.vue')), 'home')
}
復制代碼

本項目采用的是第二種方式,為了后續 webpack 打包優化。

改變單頁面應用的 title

由于單頁面應用只有一個 html,所有頁面的 title 默認是不會改變的,但是我們可以才路由配置中加入相關屬性,再在路由守衛中通過 js 改變頁面的 title

router.beforeEach((to, from, next) => {
  document.title = to.meta.title
})
復制代碼

登錄權限校驗

在應用中,通常會有以下的場景,比如商城:有些頁面是不需要登錄即可訪問的,如首頁,商品詳情頁等,都是用戶在任何情況都能看到的;但是也有是需要登錄后才能訪問的,如個人中心,購物車等。此時就需要對頁面訪問進行控制了。

此外,像一些需要記錄用戶信息和登錄狀態的項目,也是需要做登錄權限校驗的,以防別有用心的人通過直接訪問頁面的 url 打開頁面。

此時。路由守衛可以幫助我們做登錄校驗。具體如下:

1、配置路由的 meta 對象的 auth 屬性

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首頁', keepAlive: false, auth: false },
  },
  {
    path: '/mine',
    name: 'mine',
    component: () => import('../views/mine.vue'),
    meta: { title: '我的', keepAlive: false, auth: true },
  },
]
復制代碼

2、在路由首頁進行判斷。當to.meta.auth為true(需要登錄),且不存在登錄信息緩存時,需要重定向去登錄頁面

router.beforeEach((to, from, next) => {
  document.title = to.meta.title
  const userInfo = sessionStorage.getItem('userInfo') || null
  if (!userInfo && to.meta.auth) {
    next('/login')
  } else {
    next()
  }
})
復制代碼

頁面緩存配置

項目中,總有一些頁面我們是希望加載一次就緩存下來的,此時就用到 keep-alive 了。keep-alive 是 Vue 提供的一個抽象組件,用來對組件進行緩存,從而節省性能,由于是一個抽象組件,所以在 v 頁面渲染完畢后不會被渲染成一個 DOM 元素。

1、通過配置路由的 meta 對象的 keepAlive 屬性值來區分頁面是否需要緩存

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home.vue'),
    meta: { title: '首頁', keepAlive: false, auth: false },
  },
  {
    path: '/list',
    name: 'list',
    component: () => import('../views/list.vue'),
    meta: { title: '列表頁', keepAlive: true, auth: false },
  },
]
復制代碼

2、在 app.vue 做緩存判斷

<div id="app">
  <router-view v-if="!$route.meta.keepAlive"></router-view>
  <keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
  </keep-alive>
</div>
復制代碼

多環境變量配置

首先我們先來了解一下環境變量,一般情況下我們的項目會有三個環境,本地環境(development),測試環境(test),生產環境(production),我們可以在項目根目錄下建三個配置環境變量的文件.env.development,.env.test,.env.production

環境變量文件中只包含環境變量的“鍵=值”對:

NODE_ENV = 'production'
VUE_APP_ENV = 'production' // 只有VUE_APP開頭的環境變量可以在項目代碼中直接使用
復制代碼

除了自定義的 VUEAPP*變量之外,還有兩個可用的變量:

NODE_ENV : "development"、"production" 或 "test"中的一個。具體的值取決于應用運行的模式。
BASE_URL : 和 vue.config.js 中的 publicPath 選項相符,即你的應用會部署到的基礎路徑。
下面開始配置我們的環境變量

1、在項目根目錄中新建.env.*

.env.development 本地開發環境配置

NODE_ENV='development'
VUE_APP_ENV = 'development'
復制代碼

env.staging 測試環境配置

NODE_ENV='production'
VUE_APP_ENV = 'staging'
復制代碼

env.production 正式環境配置

NODE_ENV='production'
VUE_APP_ENV = 'production'
復制代碼

為了在不同環境配置更多的變量,我們在 src 文件下新建一個 config/index

// 根據環境引入不同配置 process.env.NODE_ENV
const config = require('./env.' + process.env.VUE_APP_ENV)
module.exports = config
復制代碼

在同級目錄下新建 env.development.js,env.test.js,env.production.js,在里面配置需要的變量。
以 env.development.js 為例

module.exports = {
  baseUrl: 'http://localhost:8089', // 項目地址
  baseApi: 'https://www.mock.com/api', // 本地api請求地址
}
復制代碼

2、配置打包命令

package.json 里的 scripts 不同環境的打包命令

通過 npm run serve 啟動本地
通過 npm run test 打包測試
通過 npm run build 打包正式

"scripts": {
  "dev": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "test": "vue-cli-service build --mode test",
}
復制代碼

vue.config.js 配置

vue-cli3 開始,新建的腳手架都需要我們在 vue.config.js 配置我們項目的東西。主要包括

打包后文件輸出位置
關閉生產環境 souecemap
配置 rem 轉化 px
配置 alias 別名
去除生產環境 console
跨域代理設置
此外,還有很多屬于優化打包的配置,后面會一一道來。

module.exports = {
  publicPath: './', // 默認為'/'
 
  // 將構建好的文件輸出到哪里,本司要求
  outputDir: 'dist/static',
 
  // 放置生成的靜態資源(js、css、img、fonts)的目錄。
  assetsDir: 'static',
 
  // 指定生成的 index.html 的輸出路徑
  indexPath: 'index.html',
 
  // 是否使用包含運行時編譯器的 Vue 構建版本。
  runtimeCompiler: false,
 
  transpileDependencies: [],
 
  // 如果你不需要生產環境的 source map
  productionSourceMap: false,
 
  // 配置css
  css: {
    // 是否使用css分離插件 ExtractTextPlugin
    extract: true,
    sourceMap: true,
    // css預設器配置項
    loaderOptions: {
      postcss: {
        plugins: [
          require('postcss-px2rem')({
            remUnit: 100,
          }),
        ],
      },
    },
    // 啟用 CSS modules for all css / pre-processor files.
    modules: false,
  },
 
  // 是一個函數,允許對內部的 webpack 配置進行更細粒度的修改。
  chainWebpack: (config) => {
    // 配置別名
    config.resolve.alias
      .set('@', resolve('src'))
      .set('assets', resolve('src/assets'))
      .set('components', resolve('src/components'))
      .set('views', resolve('src/views'))
 
    config.optimization.minimizer('terser').tap((args) => {
      // 去除生產環境console
      args[0].terserOptions.compress.drop_console = true
      return args
    })
  },
 
  // 是否為 Babel 或 TypeScript 使用 thread-loader。該選項在系統的 CPU 有多于一個內核時自動啟用,僅作用于生產構建。
  parallel: require('os').cpus().length > 1,
 
  devServer: {
    host: '0.0.0.0',
    port: 8088, // 端口號
    https: false, // https:{type:Boolean}
    open: false, // 配置自動啟動瀏覽器  open: 'Google Chrome'-默認啟動谷歌
 
    // 配置多個代理
    proxy: {
      '/api': {
        target: 'https://www.mock.com',
        ws: true, // 代理的WebSockets
        changeOrigin: true, // 允許websockets跨域
        pathRewrite: {
          '^/api': '',
        },
      },
    },
  },
}
復制代碼

基礎組件封裝

在開發項目過程中,通常會用到很多功能和設計相類似的組件,toast 和 dialog 組件基本是每一個移動端項目都會用到的。為了更好匹配自己公司的 UI 設計風格,我們沒有直接用 vant 的 toast 和 dialog 組件,而是自己封裝了類似的組件,可供直接調用,如:

this.$toast({ msg: '手機號碼不能為空' })
 
this.$toast({
  msg: '成功提示',
  type: 'success',
})
 
this.$dialog({
  title: '刪除提示',
  text: '是否確定刪除此標簽?',
  showCancelBtn: true,
  confirmText: '確認',
  confirm(content) {
    alert('刪除成功')
  },
})
復制代碼

效果圖如下


image
Props
image.png

dialog 傳入參數

Props
image.png
Events
image.png

webpack 可視化分析

從這里開始,我們開始進行 webpack 優化打包。首先我們來分析一下 webpack 打包性能瓶頸,找出問題所在,然后才能對癥下藥。此時就用到 webpack-bundle-analyzer 了。
1、安裝依賴

npm install webpack-bundle-analyzer -D
復制代碼

2、在 vue.config.js 配置

const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
configureWebpack: (config) => {
  if (process.env.NODE_ENV === 'production') {
    config.plugins.push(new BundleAnalyzerPlugin())
  }
}
復制代碼

打包后,我們可以看到這樣一份依賴圖


image

從以上的界面中,我們可以得到以下信息:

  • 打包出的文件中都包含了什么,以及模塊之間的依賴關系
  • 每個文件的大小在總體中的占比,找出較大的文件,思考是否有替換方案,是否使用了它包含了不必要的依賴?
  • 是否有重復的依賴項,對此可以如何優化?
  • 每個文件的壓縮后的大小。

CDN 資源優化

CDN 的全稱是 Content Delivery Network,即內容分發網絡。CDN 是構建在網絡之上的內容分發網絡,依靠部署在各地的邊緣服務器,通過中心平臺的負載均衡、內容分發、調度等功能模塊,使用戶就近獲取所需內容,降低網絡擁塞,提高用戶訪問響應速度和命中率。CDN 的關鍵技術主要有內容存儲和分發技術。

隨著項目越做越大,依賴的第三方 npm 包越來越多,構建之后的文件也會越來越大。再加上又是單頁應用,這就會導致在網速較慢或者服務器帶寬有限的情況出現長時間的白屏。此時我們可以使用 CDN 的方法,優化網絡加載速度。

1、將 vue、vue-router、vuex、axios 這些 vue 全家桶的資源,全部改為通過 CDN 鏈接獲取,在 index.html 里插入 相應鏈接。

<body>
  <div id="app"></div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
  <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
  <script src="https://cdn.bootcss.com/vuex/3.1.0/vuex.min.js"></script>
  <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
  <script src="https://cdn.bootcss.com/element-ui/2.6.1/index.js"></script>
</body>
復制代碼

2、在 vue.config.js 配置 externals 屬性

module.exports = {
 ···
    externals: {
      'vue': 'Vue',
      'vuex': 'Vuex',
      'vue-router': 'VueRouter',
      'axios':'axios'
    }
  }
復制代碼

3、卸載相關依賴的 npm 包

npm uninstall  vue vue-router vuex axios
復制代碼

此時啟動項目運行就可以了。我們在控制臺就能發現項目加載了以上四個 CDN 資源。

不過現在有不少聲音說,vue 全家桶加載 CDN 資源其實作用并不大,而且公共的 CDN 資源也沒有 npm 包那么穩定,這個就見仁見智了。所以我在源碼時新建的分支做這個優化。當項目較小的就不考慮 CDN 優化了。

當然,當引入其他較大第三方資源,比如 echarts,AMAP(高德地圖),采用 CDN 資源還是很有必要的。

gZip 加速優化

所有現代瀏覽器都支持 gzip 壓縮,啟用 gzip 壓縮可大幅縮減傳輸資源大小,從而縮短資源下載時間,減少首次白屏時間,提升用戶體驗。

gzip 對基于文本格式文件的壓縮效果最好(如:CSS、JavaScript 和 HTML),在壓縮較大文件時往往可實現高達 70-90% 的壓縮率,對已經壓縮過的資源(如:圖片)進行 gzip 壓縮處理,效果很不好。

const CompressionPlugin = require('compression-webpack-plugin')
configureWebpack: (config) => {
  if (process.env.NODE_ENV === 'production') {
    config.plugins.push(
      new CompressionPlugin({
        // gzip壓縮配置
        test: /\.js$|\.html$|\.css/, // 匹配文件名
        threshold: 10240, // 對超過10kb的數據進行壓縮
        deleteOriginalAssets: false, // 是否刪除原文件
      })
    )
  }
}
復制代碼

首頁添加骨架屏

隨著 SPA 在前端界的逐漸流行,單頁面應用不可避免地給首頁加載帶來壓力,此時良好的首頁用戶體驗至關重要。很多 APP 采用了“骨架屏”的方式去展示未加載內容,給予了用戶煥然一新的體驗。

所謂的骨架屏,就是在頁面內容未加載完成的時候,先使用一些圖形進行占位,待內容加載完成之后再把它替換掉。在這個過程中用戶會感知到內容正在逐漸加載并即將呈現,降低了“白屏”的不良體驗。

本文采用vue-skeleton-webpack-plugin插件為單頁面應用注入骨架屏。

1、在src的common文件夾下面創建了Skeleton1.vue,Skeleton2.vue,具體的結構和樣式自行設計,此處省略一萬字。。。。

2、在同級目錄下新建entry-skeleton.js

import Vue from 'vue'
import Skeleton1 from './Skeleton1'
import Skeleton2 from './Skeleton2'
 
export default new Vue({
  components: {
    Skeleton1,
    Skeleton2
  },
  template: `
    <div>
      <skeleton1 id="skeleton1" style="display:none"/>
      <skeleton2 id="skeleton2" style="display:none"/>
    </div>
  `
})
復制代碼

在vue.config.js下配置插件

const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin')
configureWebpack: (config) => {
  config.plugins.push(
    new SkeletonWebpackPlugin({
      webpackConfig: {
        entry: {
          app: path.join(__dirname, './src/common/entry-skeleton.js'),
        },
      },
      minimize: true,
      quiet: true,
      router: {
        mode: 'hash',
        routes: [
          { path: '/', skeletonId: 'skeleton1' },
          { path: '/about', skeletonId: 'skeleton2' },
        ],
      },
    })
  )
}
復制代碼

此時重新加載頁面就可以看到我們的骨架屏了。注意:一定要配置樣式分離extract: true

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。