Vue開發(fā)實(shí)戰(zhàn)-教程篇

一、前言
本文基于開源項目:

https://github1s.com/vuejs/vue

https://vuejs.org/

    最近有不少小伙伴私聊廣東靚仔,能不能出一期關(guān)于vue日常項目開發(fā)的文章,廣東靚仔整理了以往開發(fā)過的vue項目,結(jié)合目前業(yè)界主流方案,因此有了這篇文章。

目錄:
自定義Webpack和Babel配置
設(shè)計一個高擴(kuò)展性的路由
可動態(tài)改變的頁面布局
將菜單和路由結(jié)合
精細(xì)化的權(quán)限設(shè)計
使用其他第三方庫
使用Mock數(shù)據(jù)進(jìn)行開發(fā)
引入Axios
管理系統(tǒng)中使用的圖標(biāo)
定制主題及動態(tài)切換主題
國際化
構(gòu)建打包發(fā)布
做好組件的單元測試
二、現(xiàn)有方案
    目前業(yè)界有很多現(xiàn)成的解決方案,廣東靚仔列舉了幾個:


Ant Design Pro


D2 Admin截圖


soybean-admin截圖
以上都是比較穩(wěn)定的管理系統(tǒng)解決方案,有興趣的小伙伴可以去看看~下面我們一起來梳理梳理,vue實(shí)際開發(fā)的一些需要考慮到的點(diǎn)。

三、正文
    在使用vue開發(fā)我們項目的時候,一般的都是采用現(xiàn)有的開源項目,然后進(jìn)行一些改造(二次開發(fā)),來滿足我們的業(yè)務(wù)需求,這種方案效率是最高的,成本也是最低的。

下面開始講講vue實(shí)際項目開發(fā)需要關(guān)注的模塊,具體內(nèi)容如下所示:
使用Vue CLI 3快速創(chuàng)建項目
腳手架,不是本文要講的重點(diǎn),隨便看看即可~

全局安裝

 npm install -g @vue/cli
 or
 yarn global add @vue/cli
新建項目

vue create my-project
最后啟動項目,看到如下效果:

















自定義Webpack和Babel配置
webpack.config.js代碼如下:

let path = require('path');
let webpack = require('webpack');
/*
 html-webpack-plugin插件,webpack中生成HTML的插件,
 具體可以去這里查看https://www.npmjs.com/package/html-webpack-plugin
 */
let HtmlWebpackPlugin = require('html-webpack-plugin');
/*
 webpack插件,提取公共模塊
 */
let CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
let config = {
  //入口文件配置
  entry: {
    index: path.resolve(__dirname, 'src/js/page/index.js'),
    vendors: ['vue', 'vue-router','vue-resource','vuex','element-ui','element-ui/lib/theme-default/index.css'] // 需要進(jìn)行單獨(dú)打包的文件
  },
  //出口文件配置
  output: {
    path: path.join(__dirname, 'dist'), //輸出目錄的配置,模板、樣式、腳本、圖片等資源的路徑配置都相對于它
    publicPath: '/dist/',                //模板、樣式、腳本、圖片等資源對應(yīng)的server上的路徑
    filename: 'js/[name].js',            //每個頁面對應(yīng)的主js的生成配置
    chunkFilename: 'js/[name].asyncChunk.js?'+new Date().getTime() //chunk生成的配置
  },
  module: {
    //加載器
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: {
          loaders: {
            scss: 'vue-style-loader!css-loader!sass-loader', // <style lang="scss">
            sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax' // <style lang="sass">
          }
        }
      },
      {
        test: /\.html$/,
        loader: "raw-loader"
      },
      {
        test: /\.css$/,
        loader: 'style-loader!css-loader'
      },
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: ["es2015","stage-0"],
          plugins: ['syntax-dynamic-import']
        }
      },
      {
        test: /\.scss$/,
        loader: 'style-loader!css-loader!sass-loader'
      },
      {
        test: /\.(eot|svg|ttf|woff|woff2)(\?\S*)?$/,
        loader: 'file-loader'
      },
      {
        //圖片加載器,雷同file-loader,更適合圖片,可以將較小的圖片轉(zhuǎn)成base64,減少http請求
        //如下配置,將小于8192byte的圖片轉(zhuǎn)成base64碼
        test: /\.(png|jpg|gif)$/,
        loader: 'url-loader?limit=8192&name=images/[hash].[ext]'
      }
    ]
  },
  //插件
  plugins: [
    //webpack3.0的范圍提升
    new webpack.optimize.ModuleConcatenationPlugin(),
    //打包生成html文件,并且將js文件引入進(jìn)來
    new HtmlWebpackPlugin({
      filename: path.resolve(__dirname, 'dist/html/index.html'), //生成的html存放路徑,相對于path
      template: path.resolve(__dirname, 'src/html/index.html'), //ejs模板路徑,前面最好加上loader用于處理
      inject: 'body',  //js插入的位置,true/'head'/'body'/false
      hash: true
    }),
    //提取功能模塊
    new CommonsChunkPlugin({
      name: 'vendors', // 將公共模塊提取,生成名為`vendors`的chunk
      minChunks: 2, //公共模塊被使用的最小次數(shù)。配置為2,也就是同一個模塊只有被2個以外的頁面同時引用時才會被提取出來作為common chunks
      // children:true  //如果為true,那么公共組件的所有子依賴都將被選擇進(jìn)來
    }),
  ],
  //使用webpack-dev-server,啟動熱刷新插件
  devServer: {
    contentBase: path.join(__dirname, "/"),
    host: 'localhost',  //建議寫IP地址,開發(fā)時候電腦的ip地址。localhost我不知道是幻覺還是怎樣,有時候熱刷新不靈敏
    port: 9090, //默認(rèn)9090
    inline: true, //可以監(jiān)控js變化
    hot: true//熱啟動
  },
  //搜索路徑變量
  resolve: {
    alias: {
      vue: 'vue/dist/vue.js'
    },
    extensions:['.js','.scss','.vue','.json']// 可以不加后綴, 直接使用 import xx from 'xx' 的語法
  }
};
 
module.exports = config;
設(shè)計一個高擴(kuò)展性的路由
根據(jù)頁面展示結(jié)構(gòu)進(jìn)行抽象,結(jié)合業(yè)務(wù)模塊進(jìn)行的合理層級劃分

router.js代碼如下:

import Vue from 'vue';
import Router from 'vue-router'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

import NotFound from '../views/404';

Vue.use(Router);

const router =  new Router({
 mode: 'history',
 routes: [
  {path:'/',redirect: '/user/login'},
  {
  path: '/user',
  component: {render: h=>h("router-view")},
  children: [{
    path: 'login',
    name: 'index',
    component: () =>
     import( /* webpackChunkName: "user" */ '../views/User/Login')
   },
   {
    path: 'register',
    name: 'news',
    component: () =>
     import( /* webpackChunkName: "user" */ '../views/User/Register')
   },
   {
    path: '*',
    name: '404',
    component:NotFound
   }
  ]
 }]
})

router.beforeEach((to,form,next)=>{
 NProgress.start();
 next();
});

router.afterEach(() => {
 NProgress.done();
});
export default router
可動態(tài)改變的頁面布局
方案一:

定義好數(shù)據(jù)格式,一個頁面可以把它劃分成多個組件來構(gòu)成,例如一個基本的布局:

header,main,footer。那么就可以劃分成三個組件,為這三個組件添加樣式,屬性,事件。

{
   header:{
      style:{},
      property:{},
      event:{}
   },
   main:{
      style:{},
      property:{},
      event:{}
   }
}
當(dāng)數(shù)據(jù)添加進(jìn)去,生成的頁面就應(yīng)該根據(jù)這些數(shù)據(jù)來渲染

方案二:

定義模板,根據(jù)需要切換

var hdInput = {
    template: "<div><input/></div>"
};
var hdTextarea = {
    template: "<div><textarea></textarea></div>"
};
new Vue({
    el: "#hdcms",
    components: {hdInput,hdTextarea},
    data:{
        formType:"hdInput"
    }
});
將菜單和路由結(jié)合
具體方案:

1.前端在本地寫好路由表,以及每個路由對應(yīng)的角色,也就是哪些角色可以看到這個菜單/路由;
2.登錄的時候,向后端請求得到登錄用戶的角色(管理者、普通用戶);
3.利用路由攔截,根據(jù)取到的用戶角色,跟本地的路由表進(jìn)行對比,過濾出用戶對應(yīng)的路由,并利用路由進(jìn)行左側(cè)菜單渲染

一、本地寫好路由表
router/index.js

//代碼位置:router/index.js
{
  path: '',
  component: layout, //整體頁面的布局(包含左側(cè)菜單跟主內(nèi)容區(qū)域)
  children: [{
    path: 'main',
    component: main,
    meta: {
      title: '首頁', //菜單名稱
      roles: ['user', 'admin'], //當(dāng)前菜單哪些角色可以看到
      icon: 'el-icon-info' //菜單左側(cè)的icon圖標(biāo)
    }
  }]
}
二、用戶登錄,獲取用戶的角色

獲取到用戶角色,存放進(jìn)localStorage,然后跳轉(zhuǎn)主頁

//代碼位置:src/components/reLoad.vue

// axios.post('/temp',this.formModel).then(res=>{})      
// 我暫時就不模擬了,直接取
let getUserRole = this.formModel.user === 'admin' ? 'admin' : 'user'
localStorage.setItem('userRole', getUserRole)
this.$router.push({
  path: '/main'
})
三、路由攔截beforeEach,并過濾出角色對應(yīng)的路由表

關(guān)鍵技術(shù)點(diǎn)addRoutes

//代碼位置:src/permission.js

router.beforeEach((to, from, next) => {
  // 取到用戶的角色
  let GetRole = localStorage.getItem("userRole")
  // 如果登錄了
  if (GetRole !== 'unload') {
    next() //next()方法后的代碼也會執(zhí)行
    // 1.如果路由表 沒根據(jù)角色進(jìn)行篩選,就篩選一次
    if (!addRouFlag) {
      addRouFlag = true
      // 2.根據(jù)用戶的角色、和需要動態(tài)展示的路由,生成符合用戶角色的路由
      var getRoutes = baseRoleGetRouters(permissionRouter, GetRole.split(","))
      // 3.利用global屬性,讓渲染菜單的組件sideMeuns.vue重新生成左側(cè)菜單
      global.antRouter = fixedRouter.concat(getRoutes)
      // 4.將生成好的路由addRoutes
      router.addRoutes(fixedRouter.concat(getRoutes))
      // 5.push之后,會重新進(jìn)入到beforeEach的鉤子里,直接進(jìn)入第一個if判斷
      router.push({ path: to.path })
    }
  } else {
    // 用戶沒登錄,跳轉(zhuǎn)到登錄頁面
    if (to.path === '/') {
      next()
    } else {
      next('/')
    }
  }
})
精細(xì)化的權(quán)限設(shè)計
權(quán)限控制是后臺管理系統(tǒng)比較常見的需求,我們需要對某些頁面的添加權(quán)限控制,可以在路由管理中的權(quán)限做一些校驗(yàn)。
一、權(quán)限校驗(yàn)函數(shù)

getCurrentAuthority()函數(shù)用于獲取當(dāng)前用戶權(quán)限,一般來源于后臺數(shù)據(jù)
check()函數(shù)用于權(quán)限的校驗(yàn)匹配
isLogin()函數(shù)用于檢驗(yàn)用戶是否登錄
/**
*權(quán)限校驗(yàn)函數(shù)
* /src/utils,/auth.js演示使用路由管理用戶權(quán)限
**/
// 獲取當(dāng)前用戶權(quán)限
export function getCurrentAuthority(){
  return ["user"];
}
//權(quán)限校驗(yàn)
export function check(authority){
  const current getCurrentAuthority();
  return current.some(item =authority.includes(item));
}
//登錄檢驗(yàn)
export function isLogin(){
  const current getcurrentAuthority();
  return current &current[0]!="guest";
}
二、路由配置元信息

路由配置元信息meta:{ authority: ["admin"] }

/**
* 路由配置元信息
* /src/router/index.js
*/
const routes =
// 省略部分代碼
{
  path:"/"
  meta:authority:["user","admin"]}
  component:()=
  import(/*webpackChunkName:"Layout"*/"../layouts/BasicLayout")
  //省略部分代碼
},
{
  path:"/403",
  name:"403",
  hideInMenu:true,
  component:()=
  import(/*webpackChunkName:"exception"*/"@/views/Exception/403")
}
];

三、路由守衛(wèi)router.beforeEach中判斷

/**登出于形到物權(quán)限
* /src/router/index.js
*/
import findLast from "lodash/findLast";
import {check,isLogin} from "../utils/auth";
import {notification} from "ant-design-vue";
// 路由守衛(wèi)判斷權(quán)限
router.beforeEach((to,from,next)=>{
  if (to.path I==from.path){
    NProgress.start()
  }
  const record findLast(to.matched,record => record.meta.authority);
  if (record && !check(record.meta.authority)){
    if (lisLogin()&&to.path !=="/user/login"){
      next({
        path:"/user/login"
      })
    } else if(to.path1 !== "/403"){
      notification.error({
        message:"403",
        description:"你設(shè)有訪間權(quán)限,請聯(lián)系管理員"
      })
      next({
        path:"/403"
      })
    }
    NProgress.done();
  }
  next();
})
使用ECharts、Antv等其他第三方庫
根據(jù)項目要求,按需引入

使用第三方的開源庫,可以提高效率~

使用Mock數(shù)據(jù)進(jìn)行開發(fā)
一、安裝:

npm i mockjs -D
-D: 僅僅在開發(fā)環(huán)境上使用

二、項目引入:

在 main.js 文件中引入mock:import '@/mock'

三、創(chuàng)建mock文件夾

// 引入隨機(jī)函數(shù)
import { Random } from 'mockjs'
// 引入Mock
const Mock = require('mockjs')

const userListData = Mock.mock({
    'data|10': [
        {
            id: () => Random.id(),
            nickName: () => Random.cword('零一二三四五六七八九十', 3),
            phone: () => Random.integer(11111111111, 99999999999),
            tgCount: () => Random.integer(0, 200),
            earnings: () => Random.float(2000, 10000, 0, 2),
        },
    ],
})

function userList(res) {
    return {
        code: 200,
        data: userListData.data,
        message: '獲取成功',
        total: 20,
        size: 10,
        user_count: 20,
        shop_count: 20,
    }
}

const shopListData = Mock.mock({
    'data|10': [
        {
            shop_id: () => Random.id(),
            shop_name: () => Random.cword('零一二三四五六七八九十', 3),
            address: () => Random.city(true),
            shop_tel: () => Random.integer(11111111111, 99999999999),
            open_date: () => Random.date(),
            earnings: () => Random.float(2000, 10000, 0, 2),
        },
    ],
})
function shopList(res) {
    return {
        code: 200,
        data: shopListData.data,
        message: '獲取推廣店鋪成功',
        total: 20,
        size: 10,
        earnings_count: 20000,
        shopCount: 20,
    }
}
export default {
    userList,
    shopList,
}
四、定義訪問的方法,接口,請求方式,請求參數(shù)

import http from '../../plugins/http'

export const getUserList = (params) => {
    return http.get('/api/cuz/userList')
}

export const getShopListById = (id) => {
    return http.get(`/api/cuz/shopList/${id}`)
}
五、攔截匹配在api中定義的請求,并對此返回模擬出的假數(shù)據(jù)

// 引入mockjs
import Mock from 'mockjs'
// 引入模板函數(shù)類
import ratings from './modules/ratings'
import cuz from './modules/cuz'

// Mock函數(shù)
const { mock } = Mock

// 設(shè)置延時
Mock.setup({
    timeout: 400,
})

// 使用攔截規(guī)則攔截命中的請求,mock(url, post/get, 返回的數(shù)據(jù));
mock(/\/api\/ratings\/list/, 'post', ratings.list)

mock(/\/api\/cuz\/userList/, 'get', cuz.userList)
mock(/\/api\/cuz\/shopList/, 'get', cuz.shopList)
Axios
一、安裝

npm install vue-axios --save
二、main.js引入

import axios from 'axios'
Vue.prototype.$axios = axios    //全局注冊,使用方法為:this.$axios
三、使用

<script>
export default{
  data(){
    return{
      userId:666,
      token:'',
    }
  },
  created(){
    this.$axios({
      method:'post',
      url:'api',
      data:this.qs.stringify({    //這里是發(fā)送給后臺的數(shù)據(jù)
            userId:this.userId,
            token:this.token,
      })
    }).then((response) =>{          //這里使用了ES6的語法
        console.log(response)       //請求成功返回的數(shù)據(jù)
    }).catch((error) =>
        console.log(error)       //請求失敗返回的數(shù)據(jù)
    })
  }
}
</script>
四、請求攔截器

// http request 攔截器
instance.interceptors.request.use(
  config => {
    const token = sessionStorage.getItem('token')
    if (token ) { // 判斷是否存在token,如果存在的話,則每個http header都加上token
      config.headers.authorization = token  //請求頭加上token
    }
    return config
  },
  err => {
    return Promise.reject(err)
  })
五、響應(yīng)攔截器

// http response 攔截器
instance.interceptors.response.use(
  response => {
    //攔截響應(yīng),做統(tǒng)一處理
    if (response.data.code) {
      switch (response.data.code) {
        case 1002:
          store.state.isLogin = false
          router.replace({
            path: 'login',
            query: {
              redirect: router.currentRoute.fullPath
            }
          })
      }
    }
    return response
  },
  //接口錯誤狀態(tài)處理,也就是說無響應(yīng)時的處理
  error => {
    return Promise.reject(error.response.status) // 返回接口返回的錯誤信息
  })
六、在需要的頁面導(dǎo)入就可以使用了

import instance from './axios'

/* 驗(yàn)證登陸 */
export function handleLogin (data) {
  return instance.post('/ds/user/login', data)
}
管理系統(tǒng)中使用的圖標(biāo)
項目中的圖標(biāo)需要集中管理起來,方便維護(hù),減少一些圖片重復(fù)引入

如果對安全沒什么特殊要求:推薦使用iconfont

如果對安全有特別要求:把圖標(biāo)統(tǒng)一存放在內(nèi)部服務(wù)

定制主題及動態(tài)切換主題
結(jié)合ElementUI使用

(Tips: 廣東靚仔看到業(yè)界關(guān)于動態(tài)主題大約有6種方案,選了其中一種)

修改ElementUI提供的變量,先根據(jù)實(shí)際情況修改變量值

// 參考:https://element.eleme.cn/#/zh-CN/component/custom-theme
/* 改變主題色變量 */
$--color-primary: #545C64;
$--color-success: #27B6AF;
$--menu-background-color: #1D212A;
$--menu-item-font-color: #B3B8C3;
$--menu-item-hover-fill: #1F2D3D;
$--main-padding: 15px;
/* 改變 icon 字體路徑變量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// 通用的布局等樣式
@import "../common";
common.scss片段:

// 自定義變量
$---menu--inline-background-color: #13161C !default;
$---index-header-height: 50px !default;
$---padding-common: 15px !default;
$---margin-common: 15px !default;
$---border-line-color: #E6E6E6 !default;
@import "~element-ui/packages/theme-chalk/src/index";
.el-menu-item.is-active {
  color: $--color-white;
  background-color: $--menu-item-hover-fill;
  font-weight: $--font-weight-primary;
}
// .............更多見GitHub源文件
在main.js中引入

// 樣式配置
import './assets/css/main.scss'
動態(tài)主題

定義好模板主題文件,這里列舉了defaut、simple兩個主題













main.scss主要內(nèi)容:

// 實(shí)際樣式引入
.theme-simple {
  @import "src/assets/themes/simple/index";
}
.theme-default {
  @import "src/assets/themes/default/index";
}
切換主題

改變body的樣式名稱即可,調(diào)用$changeTheme(theme):

const $themeList = [
  {
    id: 'theme-default',
    name: '默認(rèn)主題'
  }, {
    id: 'theme-simple',
    name: '簡單主題'
  }
]    
Vue.prototype.$changeTheme = function (theme = $themeList[0]) {
    const body = document.querySelector('body')
    $themeList.forEach(t => {
        body.classList.remove(t.id)
    })
    body.classList.add(theme.id)
    store.dispatch('Theme/changeTheme', theme) // 暫時保存到store里面
}
Tips: 圖標(biāo)在主題樣式顯示有點(diǎn)問題,使用font-face兼容下

//***********這塊font定義是為了修復(fù)問題********************
$--font-path: '~element-ui/lib/theme-chalk/fonts';
@font-face {
  font-family: 'element-icons';
  src: url('#{$--font-path}/element-icons.woff') format('woff'), url('#{$--font-path}/element-icons.ttf') format('truetype');
  font-weight: normal;
  font-display: auto;
  font-style: normal;
}
做好國際化
i18n

一、 使用國際化來更改咱們的項目語言

簡單介紹下i18n如何用

1. 安裝:

//使用yarn
yarn add vue-i18n
//npm
npm i vue-i18n -S
2. 使用:

系統(tǒng)中使用它,必須通過 Vue.use() 明確地安裝 vue-i18n:

src/i18n/index.js

//src/i18n/index.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)
// 準(zhǔn)備翻譯的語言環(huán)境信息
const messages = {
  en: {
    message: {
      hello: 'hello world'
    }
  },
  ja: {
    message: {
      hello: 'こんにちは、世界'
    }
  }
}

// 通過選項創(chuàng)建 VueI18n 實(shí)例
const i18n = new VueI18n({
  locale: 'ja', // 設(shè)置地區(qū)
  messages // 設(shè)置地區(qū)信息
})
3. 將i18n實(shí)例掛載在Vue的option中

import Vue from 'vue'
import i18n from "./src/i18n/index.js"
new Vue({
 i18n
})
4. 視圖顯示

<div id="app">
  <p>{{ $t("message.hello") }}</p>
</div>
------------------------------------
<!-- 展示效果如下 -->
<div id="app">
 <p>hello world</p>
</div>
在插值中使用$t函數(shù)就可以了

二、vue-cli項目中使用

1. 創(chuàng)建i18n文件結(jié)構(gòu)

目錄結(jié)構(gòu)如下:



這里列舉了兩種語言分別是:en英文和zh中文

en.js

export default {
  table: { // 假如用于翻譯表格
    date: "Date",
    name: "Name",
    address: "Address"
  },
  menu: {}, // 假如項目中日后還有菜單
  tabs: {} // tab切換等
}
zh.js

export default {
  table: {
    date: "日期",
    name: "姓名",
    address: "地址"
  },
  menu: {},
  tabs: {}
}
config文件夾下面的index.js,代碼如下(二者都可以):

乞丐版:

import en from './config/en'
import id from './config/id'
import ja from './config/ja'
import ae from './config/ae'
import am from './config/am'
import ca from './config/ca'
import al from './config/al'
.....
至尊版:

import Vue from "vue"
import VueI18n from "vue-i18n"
Vue.use(VueI18n)//注入到所有的子組件

//require.context(path,deep,regExp)
//有3個方法 分別是keys()

// 至尊版
let langFileds = require.context('./config', false, /\.js$/)

let regExp = /\.\/([^\.\/]+)\.([^\.]+)$/ //正則用于匹配 ./en.js中的'en'

// regExp.exec('./en.js')

let messages = {} //聲明一個數(shù)據(jù)模型,對應(yīng)i18n中的message屬性

langFileds.keys().forEach(key => {
    let prop = regExp.exec(key)[1] //正則匹配en|zh這樣的值
    //messages[prop]相當(dāng)于 messages['en'] = {table:{...}}
    messages[prop] = langFileds(key).default

})
console.log(messages);
console.log(langFileds('./en.js'));

let locale = localStorage.getItem('lang') || "zh" //從localstorag中獲取

export default new VueI18n({
    locale,//指定語言字段
    messages//定義語言字段
})
2. 修改main.js

import Vue from 'vue'
import App from './App.vue'
import ElementUI from "element-ui" // 需要安裝 element-ui
import 'element-ui/lib/theme-chalk/index.css';
Vue.config.productionTip = false
Vue.use(ElementUI)

import i18n from "./i18n" //

new Vue({
  render: h => h(App),
  i18n // 掛載
}).$mount('#app')
3. 具體使用demo

app.vue

<template>
  <div id="app">
    <template>
      <el-table :data="tableData"
                style="width: 100%">
        <el-table-column prop="date"
                         :label="$t('table.date')"
                         width="180">
        </el-table-column>
        <el-table-column prop="name"
                         :label="$t('table.name')"
                         width="180">
        </el-table-column>
        <el-table-column prop="address"
                         :label="$t('table.address')">
        </el-table-column>
      </el-table>

    </template>
    <el-button type="primary"
               @click="change('zh')">點(diǎn)擊切換中文</el-button>
    <el-button type="primary"
               @click="change('en')">點(diǎn)擊切換英文</el-button>
    <el-button type="primary"
  </div>
</template>
 <script>
  export default {
    mounted() {
      console.log(this.$i18n.t('table.date'));
    },
    methods: {
      change(lang) { //切換方法
        localStorage.setItem('lang', lang)
        window.location.reload() //localSotrage是不響應(yīng)的,為了演示效果所以直接調(diào)用刷新
      }
    },
    data() {
      return {
        tableData: [{
          date: '2016-05-02',
          name: '王小虎',
          address: '上海市普陀區(qū)金沙江路 1518 弄'
        }]
      }
    }
  }
  </script>
  <style>
  #app {
    width: 50%;
  }
</style>
構(gòu)建打包發(fā)布
1. 打包配置如下:

build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: './',
    productionSourceMap: true,
    // 默認(rèn)情況下,Gzip 關(guān)閉了許多流行的靜態(tài)主機(jī),例如
    // Surge 或 Netlify 已經(jīng)為您壓縮了所有靜態(tài)資產(chǎn)。
    // 在設(shè)置為 `true` 之前,請確保:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],
    // 運(yùn)行帶有額外參數(shù)的構(gòu)建命令
    // 構(gòu)建完成后查看包分析器報告:
    // `npm run build --report`
    // 設(shè)置為 `true` 或 `false` 以始終打開或關(guān)閉它
    bundleAnalyzerReport: process.env.npm_config_report
  }
2. 一般部署,我們會結(jié)合Nginx一起使用

安裝&啟動

# 安裝,安裝完成后使用nginx -v檢查,如果輸出nginx的版本信息表明安裝成功
sudo apt-get install nginx
# 啟動
sudo service nginx start
3. 修改nginx配置

nginx的配置文件就在/etc/nginx文件夾

/etc/nginx/sites-available/default



nginx代理的根目錄是/var/www/html

mkdir /www
echo 'Hello world' > /www/index.html


4. 同步到服務(wù)器

在git-bash或者powershell使用scp指令,如果是linux環(huán)境開發(fā),還可以使用rsync指令:

scp -r dist/* root@117.78.4.26:/www

rsync -avr --delete-after dist/* root@117.78.4.26:/www
package.json腳本,方便,提高效率

"scripts": {
  "build": "vue-cli-service build",
  "push": "yarn build && scp -r dist/* root@117.78.4.26:/www"
},
當(dāng)然啦,對于history、與hash模式,對應(yīng)微調(diào)下即可~

做好組件的單元測試
Vue 的單文件組件使得為組件撰寫隔離的單元測試這件事更加直接

組件的單元測試有很多好處:

提供描述組件行為的文檔
節(jié)省手動測試的時間
減少研發(fā)新特性時產(chǎn)生的 bug
改進(jìn)設(shè)計
促進(jìn)重構(gòu)
一個簡單的Demo:

<template>
  <div>
    <div class="message">
      {{ message }}
    </div>
    Enter your username: <input v-model="username">
    <div
      v-if="error"
      class="error"
    >
      Please enter a username with at least seven letters.
    </div>
  </div>
</template>

<script>
export default {
  name: 'Foo',

  data () {
    return {
      message: 'Welcome to the Vue.js cookbook',
      username: ''
    }
  },

  computed: {
    error () {
      return this.username.trim().length < 7
    }
  }
}
</script>
單元測試,代碼如下:

import { shallowMount } from '@vue/test-utils'
import Foo from './Foo'

const factory = (values = {}) => {
  return shallowMount(Foo, {
    data () {
      return {
        ...values
      }
    }
  })
}

describe('Foo', () => {
  it('renders a welcome message', () => {
    const wrapper = factory()

    expect(wrapper.find('.message').text()).toEqual("Welcome to the Vue.js cookbook")
  })

  it('renders an error when username is less than 7 characters', () => {
    const wrapper = factory({ username: ''  })

    expect(wrapper.find('.error').exists()).toBeTruthy()
  })

  it('renders an error when username is whitespace', () => {
    const wrapper = factory({ username: ' '.repeat(7)  })

    expect(wrapper.find('.error').exists()).toBeTruthy()
  })

  it('does not render an error when username is 7 characters or more', () => {
    const wrapper = factory({ username: 'Lachlan'  })

    expect(wrapper.find('.error').exists()).toBeFalsy()
  })
})
Tips:   工廠函數(shù)將 values 對象合并到了 data 并返回了一個新的 wrapper 實(shí)例。好處有兩個:

1. 不需要在每個測試中重復(fù) const wrapper = shallowMount(Foo)。

2.  當(dāng)我們想為更復(fù)雜的組件在每個測試中偽造或存根一個方法或計算屬性時,你只需要聲明一次即可。

Vue Test Utils 及龐大的 JavaScript 生態(tài)系統(tǒng)提供了大量的工具促進(jìn) 100% 的測試覆蓋率。

推薦閱讀:

https://v1.test-utils.vuejs.org/zh/guides/#%E8%B5%B7%E6%AD%A5

四、總結(jié)
    在我們閱讀完官方文檔后,我們一定會進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。
    這里廣東靚仔給下一些小建議:
在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計理念、源碼分層設(shè)計
閱讀下框架官方開發(fā)人員寫的相關(guān)文章
借助框架的調(diào)用棧來進(jìn)行源碼的閱讀,通過這個執(zhí)行流程,我們就完整的對源碼進(jìn)行了一個初步的了解
接下來再對源碼執(zhí)行過程中涉及的所有函數(shù)邏輯梳理一遍

作者:廣東靚仔


歡迎關(guān)注:前端早茶