1. 程式人生 > >全棧02 Koa2+Vue2+MySQL 全棧的入門嘗試

全棧02 Koa2+Vue2+MySQL 全棧的入門嘗試

0 前言

其實並沒有想要成為全棧工程師的想法,因為我覺得自己作為一個半路出家、至今全職前端開發經驗不到兩年的程式猿來說,把前端稍微深入一點的東西搞明白就算不錯。

但是心裡其實一直有一些疑惑,比如,每次後臺提供的介面到底是怎麼工作的?資料是怎麼存放到資料庫中的?資料庫是怎麼維護的?等等。這些問題總不時的在我全心全意的碼程式碼的過程中冒出來。

幸好,我們前端有了NodeJS這個利器,讓我可以在現有的技能樹上去簡單的探討這些問題,而不用完全新開一門技能。這讓我能夠降低難度的去初步瞭解後臺乃至整個網站的開發流程,讓我有一些明白在以前的工作中接觸過的後臺程式碼“啊,原來是這麼回事”。

想著通過嘗試實現一個小小的登陸+後臺管理系統,瞭解後臺介面的實現、對資料庫的操作以及程式碼部署。這次嘗試對於我自己而言還是有意義的,補上了一些短板,解決了一些疑惑,但是這次嘗試也僅僅是初步的、淺嘗輒止的,很多更深的問題我目前沒有經歷和能力去深入的研究,所以本文也可能僅僅適用於有一些前端基礎、對全棧技能有一些好奇的小夥伴,前端骨幹、後端精英、全棧大神們,咱們說好,不要鄙視我好麼?

確定了這個基本的約定後,讓我們開始全棧開發的初次嘗試吧。

P.S. 這篇文章嘗試的主要思路來自於這篇文章,具體程式碼、實現有所不同,特此致謝。

1 專案簡介

整個專案的技術棧,前端是Vue+Vue-Router+Element,後端的話主要是Koa2+MySQL。

前端的話不用說了,後端的話主要就是兩個方面,一是為前端提供API,二是操作資料庫。

2 入口檔案

首先建立專案的入口檔案index.js

require('babel-polyfill');
require('babel-register');

if (process.env.NODE_ENV === 'development') {
  require('./server/dev');
} else {
  require('./server/app');
}

這個入口檔案作用有兩個,一個是區分開發模式和生產模式,兩個模式下面主要的區別就是監聽給的埠不同並且生產模式下會將webpack打包好的專案目錄作為Koa靜態檔案服務。

另一個作用是引入babel-registerbabel-polyfill

babel-register的目的主要是為了在檔案中相容ES6的importexport,但是注意:在入口檔案(即本專案中的index.js)中仍然不能使用。

babel-polyfill的目的就是為ES6的一些API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全域性物件,以及一些定義在全域性物件上的方法(比如Object.assign)提供轉碼。這個我考慮在後面進行優化,減小webpack打包體積,後面再說。

3 Koa中介軟體

首先看一下後端服務的真正的啟動檔案,以app.js為例吧,使用的框架是Koa2,在此基礎上引用了很多的Koa2的中介軟體

3.1 koa-router

對路由進行處理控制的中介軟體,使用的時候我們現在server資料夾下面新建一個router資料夾,用存放所有子路由的檔案

現在我們建立了兩個檔案,api.js用來管理所有的api,auth.js用來專門處理許可權相關的路由:

auth.js,很簡單,引用koa-router,然後宣告相關的路由並且對應處理方法,最後將路由模組匯出

import * as userController from '../controller/userController'
import KoaRouter from 'koa-router';

const router = new KoaRouter();

router.get('/user/:id', userController.getUserInfo);
router.post('/user', userController.postUserAuth);

export  {
  router
};

裡面引入的userController就是所有MVC中的C,controller層,我的理解就是對路由進行處理,對api請求進行處理,getUserInfopostUserAuth都是在其中定義好的方法,具體一會再看。

auth.js作為子路由匯出之後,在app.js中引入並且使用:

import KoaRouter from 'koa-router';
import * as auth from './router/auth';

const router = new KoaRouter();
const authRouter = auth.router;

// 掛載auth子路由
router.use('/auth', authRouter.routes());

// 掛載所有路由
app.use(router.routes());

這樣層層傳遞下來,當我們使用get請求訪問/auth/user時,就會觸發預定義的userController.getUserInfo方法

其他api請求也是同樣的處理。

3.2 koa-bodyparser

koa-bodyparser用來解析body的中介軟體,比方說你通過post來傳遞表單,json資料,或者上傳檔案,在koa中是不容易獲取的,通過koa-bodyparser解析之後,在koa中this.body就能直接獲取到資料。

3.3 koa-logger

koa-logger是koa的日誌模組,在安裝、掛載之後

import KoaLogger from 'koa-logger';
app.use(KoaLogger());

命令程式就會在控制檯自動列印日誌

3.4 koa-json

用來美觀的輸出JSON response,有兩種使用方式:

一種是總是返回美化了的json資料,本文就採用的這種

import KoaJson from 'koa-json';
app.use(KoaJson());

另一種是預設不進行美化,但是當地址欄傳入pretty引數的時候,則返回的結果是進行了美化的。

app.use(json({ pretty: false, param: 'pretty' }));

3.5 kcors

用來處理跨域的中介軟體,簡化了我們處理CORS跨院的設定步驟,為我們在請求頭上加上CORS,客戶端即可跨域傳送請求

import Kcors from 'kcors';

// 跨域設定
const corsOptions = {
  'origin': '',
  'credentials': true,
  'maxAge': 3600
};
app.use(Kcors(corsOptions));

3.6 koa-jwt

用來實現JSON-WEB-TOKEN的中介軟體,具體的後面關於登入的章節進行展開

import jwt from 'koa-jwt';

router.use('/api', jwt({ secret: db.jwtSecret }), apiRouter.routes()); // 所有走/api/打頭的請求都需要經過jwt驗證。

3.7 koa-compress

用來為服務端的靜態檔案開啟壓縮

import Compress from 'koa-compress';

app.use(Compress({ 
  threshold: 2048 // 要壓縮的最小響應位元組
}));

3.8 koa-static

用來實現訪問靜態資源,當我們使用webpack進行生產模式的打包之後,都放到了dist目錄下,這個目錄就作為Koa靜態檔案服務的目錄:

import staticServer from 'koa-static';

// 將webpack打包好的專案目錄作為Koa靜態檔案服務的目錄
app.use(staticServer(path.resolve('dist'))); 

4 後端服務

對資料庫的設計這裡就不講了,因為我也不會,只是簡單的設計了兩個表,一個是user,用來存放使用者名稱和密碼:

欄位 型別 說明
id int(自增) 使用者的id
username varchar(50) 使用者名稱
password char(128) 進行bcrypt加密後的密碼

另一張表是example,用來顯示一些主要的資料

欄位 型別 說明
id int(自增) 資料id
title varchar(50) 標題
image varchar(50) 插入時間

有了資料庫的基本結構後,我們需要使用NodeJS來操作資料庫,對資料庫進行增刪改查,對於Mongodb以前公司的同時使用的是Mongoose,對於MySQL我使用了Sequelize,關於Sequelize的基本使用可以參考我的另一篇總結

在使用Sequelize之前我們需要把資料庫的表結構匯出來,當然我們可以手動的匯出,但是如果表很多的情況下,手動匯出的效率太低了,可以使用sequelize-auto匯出

這之前我們需要全域性安裝sequelize-auto:

npm install sequelize-auto -g

然後安裝MySQL在NodeJS下的驅動mysql:

npm install mysql --save-dev

安裝完成之後,進入server目錄,執行下面的語句:

sequelize-auto -o "./schema" -d zhou -h 127.0.0.1 -u root -p 3306 -x XXXXX -e mysql
  • -o 引數後面的是輸出的資料夾目錄
  • -d 引數後面的是資料庫名
  • -h 引數後面是資料庫地址
  • -u 引數後面是資料庫使用者名稱
  • -p 引數後面是埠號
  • -x 引數後面是資料庫密碼,
  • -e 引數後面指定資料庫為mysql

完成之後再schema下面就會生成對應各個表的JS檔案

user.js

export default function (sequelize, DataTypes) {
  return sequelize.define('user', {
    id: {
      type: DataTypes.INTEGER(10),
      allowNull: false,
      primaryKey: true
    },
    username: {
      type: DataTypes.STRING(50),
      allowNull: true,
      defaultValue: ''
    },
    password: {
      type: DataTypes.CHAR(128),
      allowNull: true,
      defaultValue: ''
    }
  }, {
    tableName: 'user'
  });
};

example.js

export default function(sequelize, DataTypes) {
  return sequelize.define('example', {
    id: {
      type: DataTypes.INTEGER(10),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    title: {
      type: DataTypes.STRING(50),
      allowNull: true
    },
    image: {
      type: DataTypes.STRING(100),
      allowNull: true
    }
  }, {
    tableName: 'example'
  });
};

然後我們在server下建立config資料夾,在其中建立db.js檔案,用來存放資料庫地址、使用者名稱、密碼等資訊,在這個檔案中我通過對環境的判斷,讓程式碼操縱不同的資料庫,在開發環境下想測試庫寫入資訊,生產環境下操作的則是線上庫

然後再在server下面建立common資料夾,建立mysql.js檔案,用來連線我們上面定義好的資料庫:

import Sequelize from 'sequelize';
import config from '../config/db';

const mysql = new Sequelize(config.mysql.default, {
  define: {
    timestamps: false,
  },
  operatorsAliases: false
});
export default mysql;

在連線好資料庫並且匯出mysql這個物件後,我們對資料庫的增刪改查都是基於這個物件上完成的。

建立model資料夾,用來存放對資料庫處理的Model層的程式碼檔案,建立userModel.js,對user表進行處理:

import Mysql from '../common/mysql'; // 引入MySQL資料庫

const userSchema = '../schema/user'; // 引入user的表結構
const User = Mysql.import(userSchema);// 將Sequelize與表結構對應

export async function getUserById(id) {
  return await User.findOne({
    where: {
      id
    }
  })
}

export async function getUserByName(username) {
  return await User.findOne({
    where: {
      username
    }
  })
}

上面的程式碼中,首先將資料庫和user表進行了對應,使用的是sequelize的import方法,然後定義了兩個方法,分別是通過id和使用者名稱查詢資料

然後在我們上面提到的controller資料夾下的userController.js中,編寫api的邏輯。

同樣我們定義了兩個方法,第一個方法是獲取使用者資訊getUserInfo,對應的就是/auth/user/:idget請求

export async function getUserInfo(ctx) {
  const id = ctx.params.id; // 獲取url裡傳過來的引數裡的id
  const user = await userModal.getUserById(id);
  if (user) {
    ctx.body = {
      success: true,
      retDsc: '查詢成功',
      ret: user
    }
  } else {
    ctx.body = {
      success: false,
      retDsc: '使用者不存在',
      ret: null
    };
  }
}

需要注意的是,ctx是koa的上下文,可以理解為上(request)下(response)溝通的環境,其實就是將NodeJS的request/response物件封裝進一個單獨物件,ctx.req=ctx.requestctx.res=ctx.responsectx.body是http協議中的響應體,ctx.header是響應頭

url裡面傳過來的引數/user/:id是通過ctx.params.id獲取的

另外一個方法postUserAuth是用來為密碼驗證通過的使用者下發token,保證在訪問過程中使用者許可權的有效性,具體會在登入的章節展開,在這裡我們只需要瞭解這個方法的主要邏輯即可

export async function postUserAuth(ctx) {
  const data = ctx.request.body; // post過來的資料存在request.body裡
  const userInfo = await userModal.getUserByName(data.username); // 資料庫返回的資料

  if (!userInfo) {
    ctx.body = {
      success: false,
      retDsc: '使用者不存在',
      ret: null
    };
    return
  }
  if (!bcrypt.compareSync(data.password, userInfo.password)) {
    ctx.body = {
      success: false,
      retDsc: '密碼錯誤',
      ret: null
    };
    return
  }
  const userToken = {
    iss: config.userToken.iss,
    name: userInfo.username,
    id: userInfo.id,
  };
  const secret = serverConfig.jwtSecret; // 指定金鑰,這是之後用來判斷token合法性的標誌
  const token = JWT.sign(userToken, secret); // 簽發token
  ctx.body = {
    success: true,
    retDsc: '登陸成功',
    ret: {
      token,
    }
  }
}

首先呼叫model裡面定義的getUserByName,通過使用者名稱查詢使用者,找不到就返回“使用者不存在”,找到使用者對密碼校驗,如果校驗不通過返回“密碼錯誤”,通過之後下發token

對於另外的exmaplModel.jsexampleController.jsapi.js流程類似,不再展開,需要指出的幾點是

  1. 想表中插入資料的方法是create,更新的就是找到對應的物件,然後對要更新的屬性直接賦值並儲存(save),刪除資料的方法是destroy
  2. 操作資料庫的操作都是非同步函式,這裡面使用了asyncawait語法
  3. 最好將返回值的retCode和retDsc有統一的定義並統一維護
  4. 介面最好定義為使用restful風格的介面

最終在後端服務的實際啟動檔案app.js中,除了在第三章提到的載入各個中介軟體的服務,實現對應的功能,加上對錯誤和日至的處理:

// log
app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  let ms = new Date() - start;
  console.log('%s %s - %s', ctx.method, ctx.url, ms); // 顯示執行的時間
});

app.on('error', (err, ctx) => {
  console.log('server error: ', err);
});

然後通過listen方法監聽指定的埠:

app.listen(config.appServer.port, () => {
  console.log(`Koa is listening in ${config.appServer.port}`);
});

在完成這些之後我們就可以開啟後端服務了,在控制檯輸入:

node ./index.js

這樣我們就要可以開啟dev.js對應的埠,提示“koa is listening in 80”

這時候我們如果改動後端程式碼,都需要手動將node進行停止,然後再次執行上述命令開啟服務,程式碼更改才能生效,好在我們有nodemon, nodemon將監視啟動目錄中的檔案,如果有任何檔案更改,nodemon將自動重新啟動node應用程式。

如果沒有全域性安裝nodemon的話是無法在命令列直接執行下面的命令來啟動nodemon服務的:

nodemon ./index.js

我們將這條命令寫到package.json中的scripts命令中,

"scripts": {
  "server": nodemon ./index.js ",
  },

當我們執行npm run server時就會啟動nodemon服務,但是我們沒有辦法指定NODE_ENV環境變數,也就無法切換環境,所以需要在命令中增加NODE_ENV=development,再次執行時會報錯,原因是“windows不支援NODE_ENV=development的設定方式”,解決方式就是使用cross-env,安裝之後再NODE_ENV=development前面增加cross-env就可以了。

"scripts": {
  "server": "cross-env NODE_ENV=development nodemon ./index.js ",
  "start": "cross-env NODE_ENV=production nodemon ./index.js "
},

這樣執行npm run server就可以監聽開發埠8099了:“Koa is listening in 8099 for development”

P.S.

  1. 開發過程中對API的自測可以使用Postman
  2. webstorm是支援NodeJS的斷點除錯的,具體可以參考這篇文章

5 前端頁面

前端頁面不細說了,就是Vue+Vue Router+element,首先進入的是login頁面,進行登入,登入之後是一個列表頁,點選單挑資料進入詳情頁,詳情頁有三個button,分別是插入、編輯和刪除,沒什麼好講的。

要注意的是現在由/login到/admin的跳轉是單頁面應用內的跳轉,沒有向後端傳送任何請求,是從前端實現的。所以實際上在應用了後面的登陸系統後,跳轉之前是需要想後端進行判斷驗證的,根據情況不同再決定是否跳轉

6 登入系統

6.1 JSON-WEB-TOKEN

使用JSON-WEB-TOKEN可以實現無狀態的登陸,可以參考這篇文章這篇文章幫助理解

主要的登陸流程如下:

  1. 客戶端通過Post請求將使用者名稱和密碼傳送到伺服器介面(這一過程一般都是明文傳輸的,建議通過SSL加密的https協議進行傳輸,避免被監聽和中間人攻擊。
  2. 伺服器核定用使用者名稱和密碼
  3. 驗證通過之後傳送經過加密的Token返回給客戶端
  4. 客戶端收到Token後應該儲存到cookie或者storage,每次訪問指定資源都應該攜帶含有Token的cookie或者storage
  5. 後端受到請求資訊,都需要驗證TOKEN是否有效

6.2 token的請求與發放

具體到我們的程式碼中,我們已經安裝了JSON-WEB-TOEKN的koa2中介軟體koa-jwt,然後在安裝bcryptjs來為密碼加解密,bcryptjs是一個第三方密碼加密庫

正規的流程應該是當我們向資料庫新增一個使用者的時候,密碼通過bcryptjs加密後儲存到資料庫的password欄位,而登陸時傳輸的是未經加密的密碼資料(所以https是必須的)。我這裡為了省事沒有做註冊的功能,所以資料庫中的密碼是通過手動加密然後儲存的。

當伺服器收到登陸驗證的Post請求(/auth/user),會呼叫對應的postUserAuth方法

router.post('/user', userController.postUserAuth);

定義在userController.js中的postUserAuth方法會應用bcrypt的compareSync方法對密碼進行驗證。

if (!bcrypt.compareSync(data.password, userInfo.password)) {
  ctx.body = {
    success: false,
    retDsc: '密碼錯誤',
    ret: null
  };
  return
}

要注意,compareSync的第一個引數應該是未加密的資料,第二個引數是加密的資料,否則是無法驗證通過的。

驗證通過之後,向客戶端下發Token,Token由兩個部分組成:

const userToken = {
  iss: config.userToken.iss,
  name: userInfo.username,
  id: userInfo.id,
};
const secret = dbConfig.jwtSecret; // 指定金鑰,這是之後用來判斷token合法性的標誌

const token = JWT.sign(userToken, secret); // 簽發token

裡面的userToken是包含了認證使用者的一些資訊,secret是我們定義的用來加密的金鑰,是之後用來判斷token合法性的標誌,secret不應該在前端被暴露出來,後端將簽發後的token返回給客戶端:

ctx.body = {
  success: true,
  retDsc: '登陸成功',
  ret: { token}
}

瀏覽器收到token之後,將token儲存到了sessionStorage中,然後進行頁面跳轉:

sessionStorage.setItem('userToken', data.ret.token);
this.$router.push('/admin')

6.3 token的驗證

訪問哪些資源需要token的驗證呢?這也需要我們在定義koa-router時指定,在app.js

router.use('/auth', authRouter.routes());
router.use('/api', jwt({ secret: dbConfig.jwtSecret }), apiRouter.routes()); // 所有走/api/打頭的請求都需要經過jwt驗證。

我們定義,所有走/api/的路由都需要經過token的驗證,驗證的金鑰就是之前定義的secret,具體的驗證工作都是有koa-jwt幫我們完成的。

所以前端在發起API請求時,都應該在請求頭中攜帶上token,否則請求會被拒絕。為了簡化程式碼,我們在Vue router的定義檔案中使用了全域性的導航守衛router.beforeEach,在每次跳轉之前都判斷是否存在token(並且token是我們所下發的token,這是通過token解析後包含我們預定義一些資訊來進行判斷的),如果存在就在請求頭新增token:

import JsonWebToken from 'jsonwebtoken';

router.beforeEach((to, from, next) => {
  const token = sessionStorage.getItem('userToken');
  const isTokenRight = !!(token && JsonWebToken.decode(token) && (JsonWebToken.decode(token).iss === config.userToken.iss));

  // 全域性設定傳送請求header的token驗證
  if (isTokenRight) {
    Vue.prototype.$http.defaults.headers.common['Authorization'] = 'Bearer ' + token
  }
});

這樣做其實不太對,請求頭增加token其實在第一次登陸成功後就增加了,以後每次的網路請求的請求頭都會攜帶token,就算token失效也不會改變

更加合理的做法應該是在每個網路請求前都判斷一下token是否存在,如果存在就攜帶,如果不存在就不新增token

我的http請求使用了axois這個庫,並且在src/components/helper下增加了一個httpHelper.js檔案,用來對網路請求進行個性化定製,主要的定製內容有:

針對開發環境還是線上環境的URL字首進行改變等:

const sever = process.env.NODE_ENV === "development" ? config.devServer : config.appServer;
axios.defaults.baseURL = sever.protocol + sever.host + ':' + sever.port;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
axios.defaults.withCredentials = true;

此外添加了請求攔截器和響應攔截器,剛才提到的對token的判斷可以放到請求攔截器中。

// 新增請求攔截器
axios.interceptors.request.use(function (config) {
  // 傳送時展示loading
  loading = uiHelper.showLoading();
  return config;
}, function (error) {
  // 請求錯誤時取消loading
  loading.close();
  // 給出提示
  uiHelper.showMessage(errorText, 'error');
  return Promise.reject(error);
});

// 新增響應攔截器
axios.interceptors.response.use(function (response) {
  // 收到相應資料後取消loading
  if (loading) {
    loading.close();
  }
  return response;
}, function (error, text) {
  // 響應錯誤時取消loading
  if (loading) {
    loading.close();
  }
  uiHelper.showMessage(errorText, 'error');
  return Promise.reject(error);
});

6.4 跳轉攔截

現在我們每一步路由的變化都會驗證token了,但是如果我們直接手動將位址列的地址更改,還是能夠跳轉到登陸後的頁面的,所以我們需要跳轉攔截,做法就是在導航守衛的鉤子函式中進行攔截:

router.beforeEach((to, from, next) => {
  const token = sessionStorage.getItem('userToken');
  const isTokenRight = !!(token && JsonWebToken.decode(token) && (JsonWebToken.decode(token).iss === config.userToken.iss));

  // 全域性設定傳送請求header的token驗證
  if (isTokenRight) {
    Vue.prototype.$http.defaults.headers.common['Authorization'] = 'Bearer ' + token
  }

  if (to.path === '/login') { // 如果是跳轉到登陸頁的
    if (isTokenRight) { // 如果有token就轉向admin頁不返回登入頁
      next('/admin')
    } else { // 否則呆在登陸頁
      next()
    }
  } else {
    if (isTokenRight) { // 如果有token就正常轉向
      next()
    } else {
      next('/login') // 否則跳轉回登入頁
    }
  }
});

注意:一定要確保要呼叫next()方法,否則鉤子就不會被resolved。如果純粹呼叫next(path)這樣的方法最終還是會回到beforeEach()這個鉤子裡面來,如果沒有寫對條件就有可能出現死迴圈,棧溢位的情況。

至此,前後端的服務就算是都搭建起來了。

7 部署

對於一個沒有任何部署驚人的人來說,部署這個小demo也是花費了很多精力。下面就是我的部署過程

首先,你需要有一個伺服器,我選的是騰訊雲的雲伺服器,最低配置那種,一個月40多,如果是學生的話可以申請免費的(雲虛擬主機是不可以的,雲虛擬主機只能配置靜態網頁,無法自行配置node環境、部署koa的服務等)

然後,你還需要申請一個MySQL的服務,同樣我選購的是最低配置的,一個月30多

7.1 配置環境

都申請好了之後,要在虛擬伺服器上配置環境。

7.1.1NodeJS

首先就是要安裝NodeJS,要存放下載資源的目錄,我放在了usr/local/src目錄下,然後執行安裝命令:

wget http://nodejs.org/dist/v8.10.0/node-v8.10.0-linux-x64.tar.gz

上述命令是下8.10.0的64位NodeJS版本,如果你想下載其他版本,可以將命令中的兩處v8.10.0替換成其他版本號;如果你的系統是 32 位(一般是64位),也可以將x64改成x32

下載完成後,執行解壓命令:

tar -zxvf node-v8.10.0-linux-x64.tar.gz

解壓完成,可以看到當前目錄解壓後的資料夾node-v8.10.0-linux-x64,將這個資料夾重新命名:

mv node-v8.10.0-linux-x64 node

現在,重新命名後的node資料夾就是程式目錄,然後需要新增環境變數,首先在root目錄下找到.bash_profile,編輯:

vim ~/.bash_profile

找到PATH=$PATH:$HOME/bin,在後面新增路徑為:

PATH=$PATH:$HOME/bin:/usr/local/src/node/bin

儲存修改,然後過載一下

source ~/.bash_profile

現在nodenpm就可以全域性使用了

7.1.2 MySQ客戶端

利用CentOS自帶的包管理軟體Yum去騰訊雲的映象源下載安裝MySQL客戶端。

yum install mysql

輸入:

mysql -h 172.21.0.15 -P 3306 -u root -p xxxx

其中172.21.0.15是你申請的MySQL的內網地址,3306是埠號,xxxx是密碼

7.1.3 其他

其他的軟體都可以通過npm來安裝了

7.1.4 儲存映象

安裝之後,在雲主機的列表頁,操作的選項卡中的更多,選擇製作映象,輸入映象名稱和競相描述,將映象儲存,如果有新的雲主機或者需要重灌系統的時候,就可以選擇自己製作的映象來快捷的安裝依賴了。

7.2 靜態資源託管

前面提到過了,先用webpack打包出項目檔案到dist目錄中,然後使用koa-static來實現靜態資源的託管

7.3 部署執行

一般會使用pm2來管理Node應用程序,而不是直接用Node來執行,原因是pm2這類程序工具可以更方便的監控一些資訊,包括cpu,記憶體,日誌,異常,其他資訊等

安裝之後的啟動命令

pm2 start index.js

7.4 二級域名解析

網站目前可以登入了,位址列輸入http://139.199.112.184/#/admin,就可以看到登入頁面了

登入頁

但是現在我們目前只能用IP登入,因為我沒有單獨購買域名,但是我以前在百度雲購買國一個域名,oldzhou.cn,並且已經備案過,所以我想將這個demo作為二級域名解析到demo.oldzhou.cn

登入百度雲控制檯,進入域名服務域名管理,點選新增解析,在主機記錄輸入demo,記錄值輸入騰訊雲伺服器的公網IP,其他選擇預設值即可。

8 webpack打包優化

8.1 開啟Gzip

webpack.prod.conf.js中,引入了compression-webpack-plugin,用來生成一個被gzip壓縮過的JS檔案

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

可以看出來,壓縮過的vendor.js檔案由1.07M減小到了306kb

體積

然後我們配合我們在前面提到的koa-compress中介軟體,來使用壓縮後的JS檔案,這時候來看我們的網路請求情況:

網路請求

可以看到,size欄目上面的數字是傳輸的大小,是經過gzip壓縮的,下面是檔案實際的大小。

8.2 定位原因

雖然小了很多,但是這樣一個簡單的demo壓縮之後還有300kb,也是讓人無法忍受的,我們就要找出打包後的檔案到底大在哪裡

我們可以在專案的package.json檔案中注入如下命令,以方便執行她(npm run analyz),預設會開啟http://127.0.0.1:8888作為展示。

"analyz": "cross-env NODE_ENV=production npm_config_report=true npm run build"

執行之後頁面內容:

定位原因

我們發現,兩個大胖子是element-ui和vue,那就有針對性的對他們進行處理

8.2.1 element的按需載入

element提供了按需載入的方法,可以有效地減小專案體積。

npm install babel-plugin-component -save-dev

然後在.babelrc中新增下面的內容:

"plugins": [
  [
    "component", 
    {
      "libraryName": "element-ui",
      "styleLibraryName": "theme-chalk"
    }
  ]
]

然後就可以按需引入元件了,在scr/heper/elementHelper.js中,引入Vue和element的css檔案,然後按需引入我們使用的表格、按鈕等元件

import Vue from 'vue'
import 'element-ui/lib/theme-chalk/index.css';

import {
  Table, TableColumn, Row, Col,
  Input,
  Button
} from 'element-ui'

[
  Table, TableColumn, Row, Col,
  Input,
  Button
].forEach(Compo => Vue.use(Compo));

export default Vue

引入之後需要使用Vue.use或者Vue.component(Button.name, Button)來註冊元件,註冊完成後匯出Vue

在前端入口檔案main.js中引用的Vue更改為由elementHelper.js匯出的Vue,然後我們就可以再元件中使用element的元件了,比如ElButtonElTable

<ElButton type="primary" class="login-button" @click="login">登陸</ElButton>

將element改為按需載入之後,再執行分析看一下體積

按需載入後的打包體積

壓縮前的打包體積由1.07M縮小到了614kb,壓縮後的打包體積由299kb縮小到了203kb

8.2.2 外部引入模組

針對打包後的Vue比較大的問題,我們採取通過繞過webpack打包,外部引入模組的方式解決,這樣既可以減小打包後的體積,加快打包速度,同時CDN的下載速度肯定要比我目前申請的伺服器的寬頻速度要快。

既然要處理,我們將前端用的相關的包都通過外部引入

外部引入模組需要在webpack.prod.conf.js中配置externals

externals: {
  'vue': 'Vue',
  'vue-router': 'VueRouter',
  'element-ui': 'ElementUI',
  'axios': 'axios'
},

webpack可以不處理應用的某些依賴庫,使用externals配置後,依舊可以在程式碼中通過CMD、AMD或者window/global全域性的方式訪問。關於externals更多的資訊可以檢視官網上的介紹

注意,這裡面對應的屬性名稱比如VueVueRouter就是原始碼中看匯出的模組的名稱

然後在index.html中通過標籤的形式引入

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>background-demo</title>
    <link type="text/css" href="static/base.css" rel="stylesheet">
    <link href="https://cdn.bootcss.com/element-ui/2.4.0/theme-chalk/index.css" rel="stylesheet">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script>
    <script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
    <script src="https://cdn.bootcss.com/element-ui/2.4.0/index.js"></script>
    <script src="https://cdn.bootcss.com/axios/0.18.0/axios.min.js"></script>
  </body>
</html>

這樣做之後再來看一下打包後的體積:

打包後體積

壓縮前的體積已經減小到了491kb,壓縮後的體積縮小到了160kb

9 寫在最後

到這裡基本上這個小的嘗試就結束了,正如題目所說的,只是一個入門的嘗試,並且還有很多可以改進的地方,但是他解決了我很多的疑惑,讓我親身接觸到了前端之外的開發領域

學海無涯,大家加油。

參考

相關推薦

02 Koa2+Vue2+MySQL 入門嘗試

0 前言 其實並沒有想要成為全棧工程師的想法,因為我覺得自己作為一個半路出家、至今全職前端開發經驗不到兩年的程式猿來說,把前端稍微深入一點的東西搞明白就算不錯。 但是心裡其實一直有一些疑惑,比如,每次後臺提供的介面到底是怎麼工作的?資料是怎麼存放到資料庫中的?資

Get技能點 Vue2.0/Node.js/MongoDB 打造商城

針對 分離 post 第7章 簡單的 文件信息 異步 tar width 第1章 課程介紹簡單回顧前端近幾年的框架模式,了解不同時期下的框架特點。其次介紹Vue框架的背景和核心思想,以及同其它MV*框架的對比。第2章 Vue基礎從0到1,如何搭建一個簡單的Vue項目;本章

2018-9-9-01成為工程師需要掌握的技術

最近兩週看了周嘯天老師講解的一個ajax的一個視訊,系統性的學到了一些東西,然後現在做一個總結   1.如何成為一名初級的全棧開發工程師呢 ###什麼是全棧開發工程師? >全棧(全站)開發工程師(FUL

資料結構攻略--線性結構不攻自破之和佇列

      上篇部落格討論了線性結構的兩種基本的結構順序表和連結串列,它們兩者各有優缺點。總之吧,當我們要儲存容量不固定的資料結構並且要對資料進行多次插入和刪除操作時要多考慮使用連結串列結構,當只涉及對儲存的資料進行只存或只讀操作時應優先選用順序表結構。 繼續討論線性結構-

mysql 量備份和增量備份

mysql備份mysql 全量備份:vim /root/mysql_bakup.sh #!/bin/bash#Date:2017/5/2#Author:wangpengtai#Blog:http://wangpengtai.blog.51cto.com#At Sunday, we will backup th

XtraBackup量備份與恢復MySQL數據

備份 mysql xtrabackup 防偽碼:沒有相當程度的孤獨是不可能有內心的平和。1、概述Percona XtraBackup(簡稱PXB)是 Percona 公司開發的一個用於 MySQL 數據庫物理熱備的備份工具,支持 MySQl(Oracle)、Percona Server 和 Mar

mysql量備份與增量備份

mysql linux 1.全量備份 簡單的說就是將所有數據庫或一個庫全部備份。2.增量備份 從上一次全量備份之後到下一次全量備份之前都叫做增量備份。對於mysql,binlong日誌就是mysql數據,對binlong的備份就是對mysql的備份。備份的時候要鎖表,影響用戶

mysql庫備份恢復某個表

正則 signed set archive 1.4 sql IE ls -l SQ 早上小紅過來問我說網站的一個功能沒了,看了下數據庫,少了個表。好吧,心裏mmp,開始恢復數據 環境: 全庫備份 恢復某一個表 1.1 查看備份數據 [aiye@aiye mysql_back

Percona Xtrabackup備份mysql庫及指定數據庫(完整備份與增量備份)

direct 包括 ria update 格式 支持 本地 chown 一次 Xtrabackup簡介 Percona XtraBackup是開源免費的MySQL數據庫熱備份軟件,它能對InnoDB和XtraDB存儲引擎的數據庫非阻塞地備份(對於MyISAM的備份同樣需要加

安全的web服務器——使用mysqldump和mysqlbinlog實現MySQL量與增量備份

ted l數據庫 pos web服務 max 教程 sudo osi 所有 1.環境 系統是Deepin15.6,數據庫的版本號是: Server version: 5.7.18-1 (Debian) 數據庫引擎是:InnoDB。如何查看數據庫版本和數據庫引擎呢? 終端

mysql量備份及增量備份流程

日誌格式 --date nlog 超過 form 方式 basename ESS transacti 由於線上mysql數據量很大,天天進行全備份數據既浪費時間又占用資源,所以打算采用全量備份和增量備份結合的方式進行備份采用每周日全量備份、周一到周六增量備份的方式背景是從一

造成MySQL表掃描的原因

記錄 添加 its 工程師 review 全表掃描 字段 count 查詢條件 全表掃描是數據庫搜尋表的每一條記錄的過程,直到所有符合給定條件的記錄返回為止。通常在數據庫中,對無索引的表進行查詢一般稱為全表掃描;然而有時候我們即便添加了索引,但當我們的SQL語句寫的不合理的

MyBatis實戰之對映器 SSM框架之批量增加示例(同步請求jsp檢視解析) mybatis的批量更新例項 造成MySQL表掃描的原因 SSM框架實戰之整合EhCache

對映器是MyBatis最強大的工具,也是我們使用MyBatis時用得最多的工具,因此熟練掌握它十分必要。MyBatis是針對對映器構造的SQL構建的輕量級框架,並且通過配置生成對應的JavaBean返回給呼叫者,而這些配置主要便是對映器,在MyBatis中你可以根據情況定義動態SQL來滿足不同場景的需要,它比

mysql量備份、增量備份實現方法

mysql全量備份、增量備份。開啟mysql的logbin日誌功能。在/etc/my.cnf檔案中加入以下程式碼: [mysqld] log-bin = "/home/mysql/logbin.log" binlog-format = ROW log-bin-index = "/home/

mysql 外連線報錯

mysql 不支援 直接寫full outer join 或者 full join來表示全外連線但是可以用left right union right 代替。 全外連線圖(非原創圖) 下面的是全外連線例子: select * from table a A(A為別名)LEFT JOI

mysql庫搜尋指定字串

mysql全庫搜尋指定字串 DELIMITER // DROP PROCEDURE IF EXISTS `proc_FindStrInAllDataBase`; # CALL `proc_FindStrInAllDataBase` ('testdb','中'); CREATE PROCEDURE `p

MySQL文字搜尋 擴充套件搜尋 布林搜尋 FULLTEXT

總結: 全文字搜尋Match(FULLTEXT) Against(‘text’)。注意FULLTEXT索引要在導完資料後再定義FULLTEXT是哪(些)列,否則很耗時。 全文字搜尋結果輸出會按相關性排列。 擴充套件查詢Against(‘text’ WITH QUERY

mysql量備份和增量備份

mysql全量備份指令碼 #!/bin/bash Data_back="/data/data_backup/"; Date=`date +"%Y_%m_%d"` mysqldump -uroot -proot --quick --events --flush-logs --single-trans

solr之mysql量同步與增量同步

一、solr管理員命令 二、案例實戰說明(全量同步與增量同步) 一、solr管理員命令 我們在生產環境時,需要管理員維護solr伺服器的資料資訊,,那麼這裡有3種主要手段: 1.curl方式 curl http://localhost:8080/solr/updat

mysql 量備份與增量備份指令碼

  全量備份 [[email protected] scripts]# cat wqbk.sh #!/bin/bash #use mysqldump to fully backup mysql data BakDir=/mnt/sata02/backup/wq