引言
记录 vue 项目中所使用的技术细节,此文着重使用和封装层面,原理性的东西会附上参考文章链接。
建议 clone 下来代码看文章:vue-template-project
麻烦动动小手点个 star 哦。
项目初始化
技术选型
结合vue生态,此移动端项目模板使用如下技术:
- 前端框架——vue
- vue 状态管理——vuex
- vue 路由管理——vue-router
- 请求方式——axios
- 样式管理——less
- 包管理——npm/cnpm
vue-cli4搭建项目
vue-cli 工具更新很快,我们现在项目中仍使用的是 vue-cli2 ,项目中如需更新脚手架工具,按照以下步骤更新即可。
安装 Vue CLI
安装
npm install -g @vue/cli
如果存在旧版本的 vue-cli ,需先卸载再安装:
npm uninstall vue-cli -g
检测版本:
vue --version
#OR
vue -V
创建项目
运行以下命令创建一个项目:
vue create vue-webapp-template
// vue-webapp-template是你创建的项目名称
新的脚手架工具也给提供了可视化界面创建和管理项目,如需使用可视化工具搭建项目可参考——从零使用vue-cli+webpack4搭建项目。
使用命令行创建项目时,会让你选择默认或手动配置,我选的第二个手动配置(Manually),因为在项目里有轻微的强迫症,没有用到的就没有选,具体每个部分、每个目录是做什么的,这个文章讲的比较清楚——从零使用vue-cli+webpack4搭建项目。
移动端组件库选型
其实,组件库是可选可不选的,如果团队中都是大牛,而且有专门的 UI 团队设计复用组件,项目中所用之处皆已封装为组件或插件,我认为这样的团队完全不需使用外部的组件库,团队本身都代表着效率,为什么还要参考别人的效率工具。
如果不是上面那种团队,我觉得还是谦虚些,选一个组件库支持基础开发更为稳妥。毕竟,业务繁忙时,效率至上。
对比了好多个移动端组件库,对比结果如下:
在这里我们选用 vant。
项目初始化后执行:
// 安装
npm i vant -S
// 安装插件
npm i babel-plugin-import -D
// 在.babelrc 中添加配置
// 注意:webpack 1 无需设置 libraryDirectory
{
"plugins": [
["import", {
"libraryName": "vant",
"libraryDirectory": "es",
"style": true
}]
]
}
// 对于使用 babel7 的用户,可以在 babel.config.js 中配置
module.exports = {
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
style: true
}, 'vant']
]
};
// 接着你可以在代码中直接引入 Vant 组件
// 插件会自动将代码转化为方式二中的按需引入形式
import { Button } from 'vant';
基本知识
vuex数据管理
在这里只介绍项目中如何使用 vuex 进行数据管理,具体知识点请查看 官网。
store 目录的设计参考官网推荐购物车案例
vuex 执行的流程图如下:
接下来展示在组件中如何调用 state、getters、actions、mutations。
state&&getters
import { mapState, mapGetters } from 'vuex'
export default {
computed: {
// 使用对象展开运算符将此对象混入到外部对象中
// home代表是store中的哪一个模块
...mapState('home', {
// 箭头函数可使代码更简练
home1: state => state.home1
}),
...mapGetters('home', {
home1Getter: 'home1'
})
}
}
actions&&mutations
import { mapMutations, mapActions } from 'vuex'
export default {
methods: {
...mapActions('home', {
handleActions: 'getExample'
}),
...mapMutations('home', {
handleMutations: 'handleMutations'
}),
}
}
vue-router路由管理
路由管理方面采用的是一个主文件和各个模块的路由文件的方式,这样维护起来会稍微舒心一些,不至于当你接到一个项目时,几千行代码在一起,看着也不是很舒服。基础目录如下:
--router
--index.js
--home.js
--my.js
index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import home from './home'
import my from './my'
Vue.use(VueRouter)
const routes = [...home, ...my]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
在 index.js 里面可以加一些全局路由守卫的东西。
例如可以在路由的 meta 中加入每个页面的 title,然后当用户进入每个页面前,判断这个组件是否有 title 属性,如果有的话,就按照你定义的 title 进行展示。
// 设置页面title
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
}
next()
})
最常用的应该是当用户未登陆时,如果想进入某个页面,需跳转至登陆页面。
实现思路:加一个全局的登陆态,并利用前端存储保存在本地,如果未登陆跳转到登陆的页面,登陆后进入本想进入的页面。
mixin
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
如果你有一些公用的数据和方法,不想在另一个组件里面再写一遍,就可以写一个 mixin 的 js 文件,类似:
const homeMixin = {
// 在不止一个文件用到的数据
data () {
return {
homeMixin: 'test-homeMixin'
}
},
// 在不止一个文件用到的方法
methods: {
one () {
},
two () {
}
}
}
export default homeMixin
在组件中如何使用呢?
// 引入
import homeMixin from 'components/common/home.js'
// 使用
export default {
mixins:[homeMixin]
}
工具封装
axios封装(请求拦截,响应拦截)
在我这个搭建的模板项目中只是简单的做了一点封装,之后我自己用到这个模板后,也可以有更多的操作性。
import Vue from 'vue'
import axios from 'axios'
import { Toast } from 'vant'
Vue.use(Toast)
axios.defaults.headers['content-Type'] = 'application/json;charset=UTF-8' // 'Content-Type': 'application/x-www-form-urlencoded'
// 请求拦截
axios.interceptors.request.use(function (config) {
if (config.method === 'post') {
} else if (config.method === 'get') {
}
return config
}, function (error) {
return Promise.reject(error)
})
// 响应拦截
axios.interceptors.response.use(res => res, err => {
if (err && (err.toString().indexOf('500') > -1 || err.toString().indexOf('502') > -1 || err.toString().indexOf('404') > -1)) {
Toast('网络或接口异常')
return Promise.reject('网络或接口异常')
} else {
return Promise.reject(err)
}
})
api统一管控
api 是按照每个模块创建的文件。
--service
----api.js // axios封装
----homeApi.js // home模块所有的请求
----myApi.js // my模块所有的请求
homeApi.js
import './api.js'
import axios from 'axios'
/**
* get 案例
* @param options
*/
export const getExample = options => {
return axios.get('mock/home1.json', { params: options })
}
/**
* post 案例
* @param options
* @returns {*}
*/
export const postExample = options => {
return axios.post('mock/home2.json', options)
}
更为具体的 axios 封装和 api 统一管控的内容可以参考我的另一篇文章详解vue中Axios的封装与API接口的管理
常用函数封装
utils.js
模板项目中封装了一个日期格式化的函数,项目中可以根据自己的需要封装几个常用的函数。
/**
* 日期格式化 new Date(...).format('yyyy-MM-dd hh:mm:ss')
* @param fmt
* @returns {*}
*/
window.Date.prototype.format = function (fmt) {
let o = {
'M+': this.getMonth() + 1,
'd+': this.getDate(),
'h+': this.getHours(),
'm+': this.getMinutes(),
's+': this.getSeconds(),
'q+': Math.floor((this.getMonth() + 3) / 3),
'S': this.getMilliseconds()
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (var k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
}
}
return fmt
}
组件化思想
什么是组件化
组件化并不是前端所特有的,一些其他的语言或者桌面程序等,都具有组件化的先例。确切的说,只要有UI层的展示,就必定有可以组件化的地方。简单来说,组件就是将一段UI样式和其对应的功能作为独立的整体去看待,无论这个整体放在哪里去使用,它都具有一样的功能和样式,从而实现复用,这种整体化的细想就是组件化。不难看出,组件化设计就是为了增加复用性,灵活性,提高系统设计,从而提高开发效率。
简而言之:一个 .vue 文件就是一个组件
slot扩展组件
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot>
元素作为承载分发内容的出口。
它允许你像这样合成组件:
<home-header title="home标题">
<p>这是slot</p>
</home-header>
然后你在 <home-header>
的模板中可能会写为:
<div id="header">
<div class="left">{{title}}</div>
<slot></slot>
</div>
install封装插件
如果重复业务很多的话,相较于组件化,插件化无疑是更能加快开发效率的方式。
开发插件
Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象:
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或属性
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
使用插件
通过全局方法 Vue.use()
使用插件。它需要在你调用 new Vue()
启动应用之前完成:
// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)
new Vue({
// ...组件选项
})
也可以传入一个可选的选项对象:
Vue.use(MyPlugin, { someOption: true })
Vue.use
会自动阻止多次注册相同插件,届时即使多次调用也只会注册一次该插件。
Vue.js 官方提供的一些插件 (例如 vue-router
) 在检测到 Vue 是可访问的全局变量时会自动调用 Vue.use()
。然而在像 CommonJS 这样的模块环境中,你应该始终显式地调用 Vue.use()
:
// 用 Browserify 或 webpack 提供的 CommonJS 模块环境时
var Vue = require('vue')
var VueRouter = require('vue-router')
// 不要忘了调用此方法
Vue.use(VueRouter)
效率工具
rem布局——cssrem+flexble.js
移动端最常用的布局无非有这几种:响应式布局、vw + vh 布局、rem 布局、vm + rem 布局。
这里采用的是 rem 布局,使用的是淘宝出品的 Flexible.js 。
;(function(win, lib) {
var doc = win.document;
var docEl = doc.documentElement;
var metaEl = doc.querySelector('meta[name="viewport"]');
var flexibleEl = doc.querySelector('meta[name="flexible"]');
var dpr = 0;
var scale = 0;
var tid;
var flexible = lib.flexible || (lib.flexible = {});
if (metaEl) {
console.warn('将根据已有的meta标签来设置缩放比例');
var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
if (match) {
scale = parseFloat(match[1]);
dpr = parseInt(1 / scale);
}
} else if (flexibleEl) {
var content = flexibleEl.getAttribute('content');
if (content) {
var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
if (initialDpr) {
dpr = parseFloat(initialDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
if (maximumDpr) {
dpr = parseFloat(maximumDpr[1]);
scale = parseFloat((1 / dpr).toFixed(2));
}
}
}
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
docEl.setAttribute('data-dpr', dpr);
if (!metaEl) {
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
if (docEl.firstElementChild) {
docEl.firstElementChild.appendChild(metaEl);
} else {
var wrap = doc.createElement('div');
wrap.appendChild(metaEl);
doc.write(wrap.innerHTML);
}
}
function refreshRem(){
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) {
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
win.addEventListener('resize', function() {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}, false);
win.addEventListener('pageshow', function(e) {
if (e.persisted) {
clearTimeout(tid);
tid = setTimeout(refreshRem, 300);
}
}, false);
if (doc.readyState === 'complete') {
doc.body.style.fontSize = 12 * dpr + 'px';
} else {
doc.addEventListener('DOMContentLoaded', function(e) {
doc.body.style.fontSize = 12 * dpr + 'px';
}, false);
}
refreshRem();
flexible.dpr = win.dpr = dpr;
flexible.refreshRem = refreshRem;
flexible.rem2px = function(d) {
var val = parseFloat(d) * this.rem;
if (typeof d === 'string' && d.match(/rem$/)) {
val += 'px';
}
return val;
}
flexible.px2rem = function(d) {
var val = parseFloat(d) / this.rem;
if (typeof d === 'string' && d.match(/px$/)) {
val += 'rem';
}
return val;
}
})(window, window['lib'] || (window['lib'] = {}));
事实上 flexible.js 做了下面三件事:
- 动态改写标签
- 给
<html>
元素添加data-dpr属性,并且动态改写data-dpr的值 - 给
<html>
元素添加font-size属性,并且动态改写font-size的值
因为我使用的是 vsCode 编辑器,在这里介绍一下使用 vsCode 时,如何快速的将 px-->rem。
-
下载 cssrem 插件
打开 vsCode 编译器————文件————首选项————设置————搜索 cssrem ————进行设置
因为我们的设计稿尺寸是375px的,自动转换时除以37.5得到应有的 rem 值。
解决300ms延迟
移动设备上的浏览器默认会在用户点击屏幕大约延迟300毫秒后才会触发点击事件。
原因: 移动端的双击会缩放导致click判断延迟。
安装FastClick
npm i fastclick -S
调用
//jquery
<script type='application/javascript' src='/path/to/fastclick.js'></script>
$(function() {
FastClick.attach(document.body);
});
//原生js
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
//vue
import FastClick from 'fastclick'
FastClick.attach(document.body);
移动端测试工具v-console
平时在 web 应用开发过程中,我们可以 console.log 去输出一些信息,但是在移动端,也就是在手机上, console.log 的信息我们是看不到的。
这种情况下,可以选择使用 alert 弹出一些信息,但是这种方法不怎么方便,也会阻断 JS 线程,导致后面的线程都不执行,影响调试体验。
那么,如果将console.log应用到移动端呢?
需要借助第三方插件:vConsole
安装
npm install vconsole
在main.js引入
import Vconsole from 'vconsole';
new Vconsole();
在需要的地方
console.log('内容')
项目优化
使用README.md记录每次更新的内容
在我目前的团队,人员流动还是比较大的,而且我们的项目耦合度很低,可能几个页面就是一个项目,所以造成项目很多。为了其他人更快的接手一个项目,所以在开发时制定了一个规范,即在 README.md 文件中列出每个文件中需要共享的部分,以减少沟通成本。
> 项目相关备注
- 相关人员 `有多人情况下全部列出`
+ 业务分析师:
+ 前端开发人员:
+ 后台开发人员:
- 环境地址 `有更多环境依次补全, 以下详情有则补充`
* 测试环境
+ 测试环境页面访问地址:
+ 测试环境接口地址:
+ 测试环境部署方式:
* 生产环境
+ 生产环境页面访问地址:
+ 生产环境接口地址:
+ 生产环境部署方式:
- 补充说明:
- 迭代说明:
- v1.0
......
分环境打包
当我们在实际开发时,最少会分三个环境:开发环境(用于本地开发)、测试环境(模拟生产环境,上线前的测试)、生产环境。
- package.json
"scripts": {
"serve": "vue-cli-service serve --open --mode development",
"build": "vue-cli-service build",
"test": "vue-cli-service build --mode test"
},
具体请参看:详解vue-cli4环境变量与分环境打包方法
mock数据
在前端开发过程中,有后台配合是很必要的。但是如果自己测试开发,或者后台很忙,没时间,那么我们需要自己提供或修改接口。下面提供两种方式,第二种更简单,个人推荐第二种。
mock文件
- 安装
npm i mockjs -D
- 在 src 目录下新建 mock 目录
- index.js 内容如下
const Mock = require('mockjs')
Mock.mock('/test/get', 'get', require('./json/testGet'))
Mock.mock('/test/post', 'post', require('./json/testPost'))
- json 文件内容如下,以 testGet.json 为例:
{
"result": "success",
"data": {
"sex": "man",
"username": "前端林木--get",
"age": 0,
"imgUrl": ""
},
"msg": ""
}
- 在main.js入口文件中引入mock数据
if (env === 'DEV') {
require('./mock') // 引入mock数据
}
- vue 中封装,然后调用即可
export const getExample = options => {
return axios.get('/test/get', { params: options })
}
第三方接口 eolinker
需登录,没注册过的小伙伴,注册一个账号吧。
注册好后有一个默认接口,当然我们要做自己的项目。
新建项目
- 添加接口
- 自定义接口
- 使用接口
- 前端项目中,后台 url 地址,有开发版,测试版,本地版等多个版本,建议大家把开发的的URL换成 mock 的地址。
webpack 优化项目
如何提高 webpack 的打包速度
-
优化 Loader
对于 Loader 来说,影响打包效率首当其冲必属 Babel 了。因为 Babel 会将代码转为字符串生成 AST,然后对 AST 继续进行转变最后再生成新的代码,项目越大,转换代码越多,效率就越低。当然了,我们是有办法优化的。
首先我们可以优化 Loader 的文件搜索范围
module.exports = {
module: {
rules: [
{
// js 文件才使用 babel
test: /\.js$/,
loader: 'babel-loader',
// 只在 src 文件夹下查找
include: [resolve('src')],
// 不会去查找的路径
exclude: /node_modules/
}
]
}
}
对于 Babel 来说,我们肯定是希望只作用在 JS 代码上的,然后 node_modules 中使用的代码都是编译过的,所以我们也完全没有必要再去处理一遍。
当然这样做还不够,我们还可以将 Babel 编译过的文件缓存起来,下次只需要编译更改过的代码文件即可,这样可以大幅度加快打包时间
loader: 'babel-loader?cacheDirectory=true'
-
HappyPack
受限于 Node 是单线程运行的,所以 Webpack 在打包的过程中也是单线程的,特别是在执行 Loader 的时候,长时间编译的任务很多,这样就会导致等待的情况。
HappyPack 可以将 Loader 的同步执行转换为并行的,这样就能充分利用系统资源来加快打包效率了
module: {
loaders: [
{
test: /\.js$/,
include: [resolve('src')],
exclude: /node_modules/,
// id 后面的内容对应下面
loader: 'happypack/loader?id=happybabel'
}
]
},
plugins: [
new HappyPack({
id: 'happybabel',
loaders: ['babel-loader?cacheDirectory'],
// 开启 4 个线程
threads: 4
})
]
-
DllPlugin
DllPlugin 可以将特定的类库提前打包然后引入。这种方式可以极大的减少打包类库的次数,只有当类库更新版本才有需要重新打包,并且也实现了将公共代码抽离成单独文件的优化方案。
接下来我们就来学习如何使用 DllPlugin
// 单独配置在一个文件中
// webpack.dll.conf.js
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 想统一打包的类库
vendor: ['react']
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].dll.js',
library: '[name]-[hash]'
},
plugins: [
new webpack.DllPlugin({
// name 必须和 output.library 一致
name: '[name]-[hash]',
// 该属性需要与 DllReferencePlugin 中一致
context: __dirname,
path: path.join(__dirname, 'dist', '[name]-manifest.json')
})
]
}
然后我们需要执行这个配置文件生成依赖文件,接下来我们需要使用 DllReferencePlugin 将依赖文件引入项目中
// webpack.conf.js
module.exports = {
// ...省略其他配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest 就是之前打包出来的 json 文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
-
代码压缩
在 Webpack3 中,我们一般使用 UglifyJS 来压缩代码,但是这个是单线程运行的,为了加快效率,我们可以使用 webpack-parallel-uglify-plugin 来并行运行 UglifyJS,从而提高效率。
在 Webpack4 中,我们就不需要以上这些操作了,只需要将 mode 设置为 production 就可以默认开启以上功能。代码压缩也是我们必做的性能优化方案,当然我们不止可以压缩 JS 代码,还可以压缩 HTML、CSS 代码,并且在压缩 JS 代码的过程中,我们还可以通过配置实现比如删除 console.log 这类代码的功能。
-
一些小的优化点
我们还可以通过一些小的优化点来加快打包速度
- resolve.extensions:用来表明文件后缀列表,默认查找顺序是 ['.js', '.json'],如果你的导入文件没有添加后缀就会按照这个顺序查找文件。我们应该尽可能减少后缀列表长度,然后将出现频率高的后缀排在前面
- resolve.alias:可以通过别名的方式来映射一个路径,能让 Webpack 更快找到路径
- module.noParse:如果你确定一个文件下没有其他依赖,就可以使用该属性让 Webpack 不扫描该文件,这种方式对于大型的类库很有帮助
如何用 webpack 来优化前端性能
⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速⾼效。
- 压缩代码:删除多余的代码、注释、简化代码的写法等等方式。可以利⽤ webpack 的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩 JS ⽂件, 利⽤ cssnano (css-loader?minimize)来压缩 css。
- 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径。
- Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现。
- Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存。
- 提取公共第三⽅库: SplitChunksPlugin 插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码。
骨架屏展示
项目 demo 中使用的是 vant 中的 Skeleton 骨架屏
// 引入
import Vue from 'vue';
import { Skeleton } from 'vant';
Vue.use(Skeleton);
// 展示子组件
// 将loading属性设置成false表示内容加载完成,此时会隐藏占位图,并显示Skeleton的子组件
<van-skeleton
title
avatar
:row="3"
:loading="loading"
>
<div>实际内容</div>
</van-skeleton>
export default {
data() {
return {
loading: true
}
},
mounted() {
this.loading = false;
}
};
如果想自己搭建一个骨架屏,给大家几个参考链接:
总结
以上就是 vue 移动端整体大的项目架构设计了(webpack 有一部分没做演示,有需要的童鞋要实践一下哦),总结一篇文章好辛苦。
2020-1-10 0:55
晚安~