LiveQing 高性能流媒体服务器前端架构概述
LiveQing 高性能流媒体服务器前端部分最初采用的是 AdminLTE + 各方 jQuery 插件的开发方式, 也就是网络上通常讲的 bootstrap + jquery plugins 的方式. 有经验的前端开发者想必都了解这种架构下开发前端页面的痛点. 当一个页面上 UI 组件多起来的时候, 代码组织就容易变得混乱, 各种 $(document).on 穿梭其中. 这样的页面开发好以后, 隔一段时间, 再来二次开发, 我去, 简直了.
为了解决这样的痛点, 我想重构前端, 引入 vue 的组件化开发模式, 借助 element-ui 这样的组件库, 可以用极少的代码, 码出丰富的功能. 这篇博客是 LiveQing 高性能流媒体服务器前端重构系列博客的第一篇: 从零开始搭建 webpack + vue + AdmintLTE 多页脚本架.
安装前端开发脚手架
首先, 从零搭建 webpack 脚手架. 这里不借助 vue-cli 工具来生成脚手架, 而是一步步从 npm install 开始到手写配置脚本. 因为, 我觉得 vue-cli 一下子生成出来那么多的配置文件和目录, 会让初学者眼花缭乱, 抓不住重点.
初始化工程目录
node -v
v6.10.0
npm -v
5.3.0
mkdir easydss-web-src
cd easydss-web-src
npm init -y
安装基础包
npm i admin-lte font-awesome vue vuex webpack webpack-dev-server --save-dev
vuex : 用于 vue 组件间的状态同步 font-awesome : 各种图标
安装常用的 webpack loader
npm i file-loader url-loader css-loader less less-loader style-loader vue-loader vue-template-compiler --save-dev
npm i babel-core babel-loader babel-preset-es2015 babel-preset-stage-2 babel-polyfill --save-dev
file-loader : 处理资源文件, 比如图片, 字体等 url-loader : 对 file-loader 的封装, 针对小图片资源提供 base64 data blob css-loader : 处理 css 文件中的 url 等 style-loader : 将 css 插入到页面的 style 标签 less-* : 将 less 转成 css vue-* : 处理 vue 单文件组件 babel-* : es6 语法支持, 详细说明参考阮一峰的 Babel 入门教程
安装常用的 webpack 插件
npm i clean-webpack-plugin html-webpack-plugin --save-dev
clean-webpack-plugin : 用来清空发布目录 html-webpack-plugin : 用来生成入口页面, 自动引入生成的 js 文件
工程目录结构预览
首先, 看一下最终的工程目录结构和运行效果, 做到心中有数. 后面将介绍这些目录文件是如何一步步创建或生成的.
liveqing-web-src [工程根目录]
├── .babelrc [babel全局配置文件]
├── dist [发布目录]
├── package.json
├── package-lock.json
├── src [源文件目录]
│ ├── about.js
│ ├── assets [资源文件目录]
│ │ └── images [资源图片]
│ ├── components [组件目录]
│ │ ├── About.vue
│ │ ├── AdminLTE.vue
│ │ ├── Index.vue
│ │ ├── NaviBar.vue
│ │ └── Sider.vue
│ ├── index.html
│ ├── index.js
│ └── store [状态管理]
│ └── index.js
└── webpack.config.js [webpack 配置文件]
babel 配置
在工程根目录下新建文件 .babelrc , 内容比较少, 如下:
{
"presets": [
"es2015",
"stage-2"
],
"plugins": []
}
webpack 配置
重头戏来了, 在工程根目录下新建文件 webpack.config.js , 内容如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const webpack = require('webpack');
const path = require('path');
require("babel-polyfill");
function resolve(dir) {
return path.resolve(__dirname, dir)
}
module.exports = {
//定义页面的入口, 因为js中将要使用es6语法, 所以这里需要依赖 babel 垫片
entry: {
index: ['babel-polyfill', './src/index.js'],
about: ['babel-polyfill', './src/about.js']
},
output: {
path: resolve('dist'), // 指示发布目录
filename: 'js/[name].[chunkhash:8].js' //指示生成的页面入口js文件的目录和文件名, 中间包含8位的hash值
},
//下面给一些常用组件和目录取别名, 方便在js中 import
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.common.js',
'jquery$': 'admin-lte/plugins/jQuery/jquery-2.2.3.min.js',
'src': resolve('src'),
'assets': resolve('src/assets'),
'components': resolve('src/components')
}
},
module: {
//配置 webpack 加载资源的规则
rules: [{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src')]
}, {
test: /\.vue$/,
loader: 'vue-loader'
}, {
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.less$/,
loader: "less-loader"
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=images/[name].[hash:8].[ext]'
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=fonts/[name].[hash:8].[ext]'
},
{
test: /\.(swf|mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader?limit=10000&name=media/[name].[hash:8].[ext]'
}]
},
plugins: [
//引入全局变量
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
"window.jQuery": 'jquery',
"window.$": 'jquery'
}),
//编译前先清除 dist 发布目录
new CleanWebpackPlugin(['dist']),
//生成视频广场首页, 在这个页面中自动引用入口 index --> dist/js/index.[chunkhash:8].js
//以 src/index.html 这个文件作为模板
new HtmlWebpackPlugin({
filename: 'index.html',
title: '视频广场',
inject: true, // head -> Cannot find element: #app
chunks: ['index'],
template: './src/index.html',
minify: {
removeComments: true,
collapseWhitespace: false
}
}),
//生成版本信息页面, 在这个页面中自动引用入口 about --> dist/js/about.[chunkhash:8].js
//以 src/index.html 这个文件作为模板
new HtmlWebpackPlugin({
filename: 'about.html',
title: '版本信息',
inject: true,
chunks: ['about'],
template: './src/index.html',
minify: {
removeComments: true,
collapseWhitespace: false
}
})
]
};
创建网页模板文件
上面 webpack.config.js 中, 我们声明了需要生成两个页面, 都是以 src/index.html 作为模板文件, 实际上我们最终生成的两个发布页面 dist/index.html 和 dist/about.html 就是在这个模板文件基础上, 插入 js 入口文件引用生成出来的(HtmlWebpackPlugin 配置项中的 inject).
下面创建这个模板文件:
src/index.html
<html>
<head>
<title><%= htmlWebpackPlugin.options.title %></title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
</head>
<body class="skin-green sidebar-mini">
<div id="app"></div>
</body>
</html>
title 部分将会被 HtmlWebpackPlugin 中的 title 替换 声明 #app div 用来挂载 vue 根组件
创建入口 js 文件
有了网页模板文件, 接下来我们要编写入口 js 文件了. 在入口 js 文件里面, 我们创建 vue 根组件, 并将它挂载到模板页面的 #app 上面.
先贴出两个入口 js 内容, 再作说明.
src/index.js
import Vue from 'vue'
import store from "./store";
import AdminLTE from './components/AdminLTE'
import Index from './components/Index'
new Vue({
el: '#app',
store,
template: `
<AdminLTE>
<Index></Index>
</AdminLTE>`,
components: {
AdminLTE, Index
},
})
src/about.js
import Vue from 'vue'
import store from "./store";
import AdminLTE from './components/AdminLTE'
import About from './components/About'
new Vue({
el: '#app',
store,
template: `
<AdminLTE>
<About @btnClick="btnClick"></About>
</AdminLTE>`,
components: {
AdminLTE, About
},
methods: {
btnClick(msg){
alert(msg);
}
}
})
两个 vue 根组件, 共同的地方是 :
- 都引用了 vuex store 状态管理, 我们用它来保存各个页面或组件之间共用的数据;
- 都引用了 AdminLTE 这个子组件; 实际上在这个子组件里面, 我们定义了 AdminLTE 的整体布局, 先是顶部导航和左侧菜单栏占位, 然后预留一个 slot 私有内容区域, 用以展示各个页面不同的内容; 顺带说一下, about 页面中演示了 父子组件间的数据交互
创建 vuex store
vuex store 中的数据在整个组件树中共享, 只需要在根组件中引用一个 store. 子组件中通过 mapState, mapGetters, mapMutations, mapActions 访问和修改. vuex 官方文档传送门.
这里, 暂时想到的共享数据仅仅包括 左上角的 logo 和 左侧栏的菜单数据, 所以我们的 store 文件很简单:
store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
logoText: "EasyDSS",
logoMiniText: "DSS",
menus: [
{
path: "/index.html",
icon: "mouse-pointer",
text: "视频广场"
}, {
path: "/about.html",
icon: "support",
text: "版本信息"
}
]
},
getters : {
},
mutations: {
},
actions : {
}
})
export default store;
创建子组件
- AdminLTE.vue
引入 adminlte 样式和脚本文件, 指定界面布局, 预留 slot 内容区
components/AdminLTE.vue
<template>
<div class="wrapper">
<NaviBar :logoText="logoText" :logoMiniText="logoMiniText"></NaviBar>
<Sider :menus="menus"></Sider>
<div class="content-wrapper">
<section class="content">
<slot></slot>
</section>
</div>
</div>
</template>
<script>
import "font-awesome/css/font-awesome.min.css";
import "admin-lte/bootstrap/css/bootstrap.min.css";
import "admin-lte/dist/css/AdminLTE.min.css";
import "admin-lte/dist/css/skins/_all-skins.css";
import "admin-lte/bootstrap/js/bootstrap.min.js";
import "admin-lte/dist/js/app.js";
import { mapState } from "vuex"
import Vue from 'vue'
import Sider from './Sider'
import NaviBar from './NaviBar'
export default {
data() {
return {
}
},
components: {
NaviBar, Sider
},
computed: {
//访问 vuex store 中的数据
//此处用到 es6 stage-2 才有的三个点展开对象的语法, 对应 .babelrc 中的配置
...mapState([
"logoText",
"logoMiniText",
"menus"
])
}
}
</script>
- NaviBar.vue
顶部导航组件, 主要是 logo 和 菜单栏的 toggle, 数据从 AdminLTE 组件传入
components/NaviBar.vue
<template>
<header class="main-header">
<a href="index.html" class="logo">
<span class="logo-mini">{{logoMiniText}}</span>
<span class="logo-lg">{{logoText}}</span>
</a>
<nav class="navbar navbar-static-top">
<a class="sidebar-toggle" data-toggle="offcanvas" role="button">
<span class="sr-only">Toggle navigation</span>
</a>
</nav>
</header>
</template>
<script>
export default {
props: {
logoText: {
default: "AdminLte"
},
logoMiniText: {
default: "AD"
}
}
}
</script>
- Sider.vue
左侧菜单栏组件 , 菜单数据从 AdminLTE 组件传入, 通过比较浏览器地址栏 path , 决定 active 菜单项
components/Sider.vue
<template>
<aside id="slider" class="main-sidebar">
<section class="sidebar">
<ul class="sidebar-menu">
<li :class="['treeview', path == item.path ? 'active' : '']" v-for="(item,index) in menus" :key="index">
<a :href="item.path">
<i :class="['fa', 'fa-' + item.icon]"></i>
<span>{{item.text}}</span>
</a>
</li>
</ul>
</section>
</aside>
</template>
<script>
export default {
props: {
menus : {
default : () => []
}
},
computed: {
path(){
return location.pathname;
}
}
}
</script>
- Index.vue
首页内容区
components/Index.vue
<template>
<div class="container-fluid no-padding">
<div class="alert alert-success">{{msg}}</div>
</div>
</template>
<script>
export default {
data() {
return {
msg : "我是视频广场"
}
}
}
</script>
- About.vue
版本信息内容区
components/About.vue
<template>
<div class="container-fluid no-padding">
<button class="btn btn-success" @click.prevent="btnClick">{{btnText}}</button>
</div>
</template>
<script>
export default {
props: {
btnText : {
type : String,
default : ""
}
},
methods: {
btnClick(){
this.$emit("btnClick", "hello");
}
}
}
</script>
<style lang="less" scoped>
</style>
运行和编译
编辑 package.json, 添加运行和编译脚本指令, 留意其中的 scripts > build, start
{
"name": "liveqing-web-src",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --progress --hide-modules",
"start": "webpack-dev-server --open",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"admin-lte": "^2.3.11",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.1",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"clean-webpack-plugin": "^0.1.16",
"css-loader": "^0.28.5",
"file-loader": "^0.11.2",
"font-awesome": "^4.7.0",
"html-webpack-plugin": "^2.30.1",
"less": "^2.7.2",
"less-loader": "^4.0.5",
"style-loader": "^0.18.2",
"url-loader": "^0.5.9",
"vue": "^2.4.2",
"vue-loader": "^13.0.4",
"vue-template-compiler": "^2.4.2",
"vuex": "^2.3.1",
"webpack": "^3.5.5",
"webpack-dev-server": "^2.7.1"
}
}
命令行执行 :
npm run start #自动打开浏览器, 查看页面效果
npm run build #生成发布文件到 dist 目录
总结
以上, 我们从零开始, 创建了一个 webpack + vue + AdminLTE 多页面工程的脚手架. 在此基础上可以体验 vue 组件化前端开发的简洁和高效了.
LiveQing高性能流媒体服务器前端重构(二): webpack + vue + AdminLTE 多页面提取共用文件, 优化编译时间
LiveQing高性能流媒体服务器前端重构(三): webpack + vue + AdminLTE 多页面引入 element-ui
LiveQing高性能流媒体服务器前端重构(四): webpack + video.js 打造流媒体服务器前端