云南之旅——玉龙雪山

回过头来看,照片里的我竟然还那么自信。 这是刚从大索道出来时还能笑的出来时的拍摄的。谁知往后,同行的四人只有三 […]

回过头来看,照片里的我竟然还那么自信。

这是刚从大索道出来时还能笑的出来时的拍摄的。谁知往后,同行的四人只有三人坚持到了山顶——面对传说中的4680留下纪念。征服10月的玉龙雪山十分不易,除了惊叹大自然的鬼斧神工,更多的是在茫茫的大雾中以及无情散落的冰雨里的无助,更不幸的是下来后四人都成了高原反应患者……

敬畏自然,崇尚自然或许是此次云南之旅的主题。

wordpress日常建设与维护[回首和二期计划]

最近一段时间着实花了不少功夫在改版自己的博客,博客的主题基于「anissa」,而今已经快看不出其身影。回首一期 […]

最近一段时间着实花了不少功夫在改版自己的博客,博客的主题基于「anissa」,而今已经快看不出其身影。回首一期主要改进了如下几方面:

  1. 引入图标字体库「lalaksks」和非常规字体「Play」。在阿里旗下的iconfont.cn建立了图标字体库「lalaksks」,虽然短时间内没有自己设计图标的打算,但是能按照自己的审美和喜好挑选合适的icon整理成库也足以让我沾沾自喜,「Play」字体则是借鉴别的博主,通过链接引入字体资源;
  2. 优化和细化动画。主要是针对右侧的侧边栏和头部的导航。侧边栏动画改版后还比较满意,但是无论从理性和专业的角度,还是肉眼可见的滚动时的不流畅,都在陈述着一个事实,动画太占用资源,亦或者我自己写的太糟糕;
  3. 部分页面结构的重构。主要针对评论区和文章列表页。视图层面主要倾向于扁平化和图标丰满,逻辑层面主要考虑展示更多有益于我掌握文章受欢迎度的权重等;
  4. 精简插件的使用,除了第三方登录及分享插件、emoji插件、代码高亮插件是新引入;其他很多插件已被下架;

规划一下建站二期的主要方向:

  1. 头部菜单重构以及响应式布局重新定义。样式层面考虑写两套代码,适配初步计划以1000px为界限划分PC端和移动端;
  2. 精简自身代码。wordpress框架本身的代码量较庞大,因此在不同领域才能做到百花齐放,但是作为一个博客首要的目的是记录和分享以及阅读,是否在钻进底层代码的海洋的同时也要时刻警惕本末倒置呢?除了深层次的思考,目前自己添加进主题的代码也有很多比较冗余,「编程是一门艺术」,我也时刻需要有工匠的思维去编写代码;
  3. 动手实现前后端分离。不要忘记ajax这门划时代意义的技术,给予以后维护方便的同时也让代码更具有层次感。考虑先在文章列表页获取文章详情渲染到当前的DOM,实现仿知乎浏览的方式;
  4. 多动手,少空想。

 

可爱的你

又是周一,现在感觉每一天的时间都过得异常快,有时候竟让人产生咱们还是第一天认识的幻觉。 昨晚被老鼠吵了一整晚, […]

又是周一,现在感觉每一天的时间都过得异常快,有时候竟让人产生咱们还是第一天认识的幻觉。

昨晚被老鼠吵了一整晚,今早起来心情不是很佳。想想果真如你所说的,我们捉了他弟弟,明摆着回来报复的🙇 ,得想个根本解决之法,不然感觉睡不好觉了,这个周末计划计划来个大扫除怎么样?

咕咚一溜身起床来做早餐,一来为了我周末的任性道歉,二来想早点起床做完早餐好把餐具顺带洗了(为了避免引🐱 )在此吐槽一番,是谁说的薯条要放冰箱加盐和淀粉炸起来才会脆,早上起来都出水而且很多变黑了,差评(*`Ω´*)v。而且早餐还蒸了馒头。

精彩的一幕来了,一同急匆匆出门,路上看见8路车,你又是一阵狂奔,活脱像个小孩,很逗。我撑着伞在路上和你发短信,这个感觉有点特别,哈哈,煽情了。

行了,苹果记得吃,顺带给午餐一个评价哦,薯条🍟 看看是不是和你想的一个样。

 

 

 

如何给 WordPress 添加 默认 Gravatar 头像

引言:如果不是通过第三方登录的用户,无法通过接口获取其头像,wp自身的头像模块比较封闭,可用头像无非就是神秘人 […]

引言:如果不是通过第三方登录的用户,无法通过接口获取其头像,wp自身的头像模块比较封闭,可用头像无非就是神秘人士、小怪物、空白等几个歪瓜裂枣,实在无法达到笔者对UI设计层面整体感的满足。

网上流传比较多的方法无非就是通过插件和引入函数。玩过一阵子博客后,知晓插件太占用资源,因此能不用插件的地方就避免使用。引入函数的方法经笔者实践后发现已经不可用,但是改进方法是基于原方法修改的,故介绍下。不可用原因猜测与wp版本更新后部分API改动有关,导致图片的地址有误。

wordpress默认评论头像的修改方法:

1、将头像文件保存在主题文件夹下的images文件夹中,命名为wp-default-gravatar.png;

2、在functions.php文件中加入如下代码:

add_filter( 'avatar_defaults', 'wpb_new_gravatar' );   
function wpb_new_gravatar($avatar_defaults) {  
$myavatar =  get_bloginfo('template_directory') . '/images/wp-default-gravatar.png';  
    $avatar_defaults[$myavatar] = "默认头像";  
    return $avatar_defaults;  
}

而后经过一段时间的搜索和浏览,最终总结方法如下:

1、图片上传可采用自己wp站点的后台上传或者通过wp官方「合作伙伴」gravatar 来注册登录后上传添加。改进方法:

2、同样在function.php添加代码,但是图片路径采用上传后的绝对地址:

add_filter( 'avatar_defaults', 'wpb_new_gravatar' );
function wpb_new_gravatar ($avatar_defaults) {
$myavatar = 'https://en.gravatar.com/userimage/125992477/fcde332c16644204fc0be461a5b36857?size=80';
    $avatar_defaults[$myavatar] = "默认头像";
    return $avatar_defaults;
}

3、在后台 设置 – 讨论 – 默认头像 中可见,即代表引入头像成功。

 

 

 

 

 

可以看出改进后的方法也是无奈之举,希望往后能有更优方案。

参考链接:https://wpfoot.com/6511.html

《西红市首富》与《夏洛特烦恼》观影体验

「有幸」先后观影《西红市首富》和《猛虫过江》两部关于底层人物,通过继承遗产一夜暴富为题材展开故事的电影。虽说都 […]

「有幸」先后观影《西红市首富》和《猛虫过江》两部关于底层人物,通过继承遗产一夜暴富为题材展开故事的电影。虽说都是老梗,但是,如果从观影体验和回味余韵上来讨论,前者彰显出的魔幻现实主义色彩让观影者不唐突,后者强行玩东北文化和闽南文化的冲突以制造笑料这点却让我全程尴尬。我甚至怀疑后者的团队究竟清不清楚自身作品的立意,不论剧本质量和演员演技等等。即使女主都是宋芸桦扮演,但是我觉得都不应该拿这两部剧再相提并论了。《西红市首富》应该和同位闫飞、彭大魔出品的《夏洛特烦恼》讨论。

闫飞和彭大魔的这部作品从一众演员和剧本保留的线索上,让主流媒体都将之视作《夏洛特烦恼》的续集,都是落魄到底的小角色在外界的助力下翻身的剧本。两部作品存在很多相似之处,例如前作是才华溢出,此作是金钱溢出;前作是夏洛在噩梦尽头后回到现实,和马冬梅如胶似漆的喜剧结尾,此作是王多鱼在最终考验中——金钱和人(ai)性(qing)中权衡,最终选择了后者并迎来幸(shuang)福(shou)的生活;沈腾饰演的主角本身具备不凡的小品功底,对从错愕到从容的落差拿捏得到位,剧本对故事的走向在正确的方向,但是如果仅仅如此,两部作品不会让我如此惊艳。

喜剧的内核往往是悲剧,两位作者深谙此道。小人物最后真的幸福了吗?这个问题的答案也许我们只能在生活中查寻。如果你反复看过周星驰的作品,可能会对这点更有体会。作为当代的喜剧大师,周先生的作品独一无二,正是因为他赋予了作品喜剧的身躯,悲剧的灵魂。

我觉得我对喜剧的理解还很狭隘,但是我坚信如果一部喜剧作品能让人开怀大笑后不禁沉思,它会是优秀的喜剧。

从”爱心便当”到”初级厨男”

早上恍惚中拿起手机看了眼时间,心里暗想:不慌,才7:20,继续躺会。然而不过一分钟,我便一个激灵从床上弹起来。 […]

早上恍惚中拿起手机看了眼时间,心里暗想:不慌,才7:20,继续躺会。然而不过一分钟,我便一个激灵从床上弹起来。原来我已经化身成为了小厨,要准备自己的午餐了。

现在吃过午餐再回顾,自己为自己准备午餐也是快事。除了菜式完全由自己掌握之外,分量、口味等也尽在“掌握之中”。一直以来,真·甜番茄炒蛋就是我的拿手好菜,但是苦于没有机会施展,现在自己下厨,还不是“信手拈来”?一个番茄,两个鸡蛋,加上一小份猪肉,各自经过加热去皮,搅拌,腌制的流程,猪肉先煎上盆,鸡蛋配合番茄再登场,而后我的午餐便大功告成。在一旁酣睡的丫头时不时传来一句:“用不用帮忙呀”,当真想太多了。

废话不多说,上图。

PS:附上丫头昨晚做得海鲜煲,整体尚可,入味略欠。孰强孰弱,高下立判。

有始有终

建站不易,安得始终? 近来发现自己在博客建设时间与胃口在同步增长,仿佛自己的能力已经跟不上胃口的扩容。难处主要 […]

建站不易,安得始终?

近来发现自己在博客建设时间与胃口在同步增长,仿佛自己的能力已经跟不上胃口的扩容。难处主要体现在:

  • 在参考过别人的主题过后,难免嫌弃进而无法正视自己当前的主题(感觉主题一词表达不出Theme的意味),东搬西凑一番,却越看越不顺眼;
  • 自己每次投入的时间都太零碎,要么是游戏之间的间隙,要么是工作时抽空完善,缺乏系统和专注;
  • 再者此博客的Theme是基于Anissa完善的,改建的难度难免大于新建,每一块砖头、每一颗螺丝它的意义我都需要去理解,一旦破坏某个细节说不定整层楼都会倒坍。

目前看来,要想摆脱当前的困境,也有以下几方面需要我勉励:

  • 最重要的是改变当前的心态,要考虑到自身的硬件并不足以担任合格的设计师,在UI层面无法给予自己最大的支持也是情有可原;
  • 要时刻清醒建设博客的本心。创造一个环境输出想法和文字、输入资源和提高技术;
  • 贪心不足蛇吞象,放宽心态很重要。

 

浅谈使用 Vue 构建前端 10w+ 代码量的单页面应用开发底层

开始之前 随着业务的不断累积,目前我们 ToC 端主要项目,除去 node_modules, build 配置 […]

开始之前

随着业务的不断累积,目前我们 ToC 端主要项目,除去 node_modulesbuild 配置文件dist 静态资源文件的代码量为 137521 行,后台管理系统下各个子应用代码,除去依赖等文件的总行数也达到 100万 多一点。

代码量意味不了什么,只能证明模块很多,但相同两个项目,在运行时性能相同情况下,你的 10 万行代码能容纳并维护 150 个模块,并且开发顺畅,我的项目中 10 万行代码却只能容纳 100 个模块,添加功能也好,维护起来也较为繁琐,这就很值得思考

本文会在主要描述以 Vue 技术栈技术主体ToC 端项目业务主体,在构建过程中,遇到或者总结的点(也会提及一些 ToB 项目的场景),可能并不适合你的业务场景(仅供参考),我会尽可能多的描述问题与其中的思考,最大可能的帮助到需要的同学,也辛苦开发者发现问题或者不合理/不正确的地方及时向我反馈,会尽快修改,欢迎有更好的实现方式来 pr

Git 地址
React 项目

可以参考蚂蚁金服数据体验技术团队编写的文章:

本文并不是基于上面文章写的,不过当时在看到他们文章之后觉得有相似的地方,相较于这篇文章,本文可能会枯燥些,会有大量代码,同学可以直接用上仓库看。

① 单页面,多页面

首先要思考我们的项目最终的构建主体单页面,还是多页面,还是单页 + 多页,通过他们的优缺点来分析:

  • 单页面(SPA)
    • 优点:体验好,路由之间跳转流程,可定制转场动画,使用了懒加载可有效减少首页白屏时间,相较于多页面减少了用户访问静态资源服务器的次数等。
    • 缺点:初始会加载较大的静态资源,并且随着业务增长会越来越大,懒加载也有他的弊端,不做特殊处理不利于 SEO 等。
  • 多页面(MPA)
    • 优点:对搜索引擎友好,开发难度较低。
    • 缺点:资源请求较多,整页刷新体验较差,页面间传递数据只能依赖 URLcookiestorage 等方式,较为局限。
  • SPA + MPA
    • 这种方式常见于较老 MPA 项目迁移至 SPA 的情况,缺点结合两者,两种主体通信方式也只能以兼容MPA 为准
    • 不过这种方式也有他的好处,假如你的 SPA 中,有类似文章分享这样(没有后端直出,后端返 HTML 串的情况下),想保证用户体验在 SPA 中开发一个页面,在 MPA 中也开发一个页面,去掉没用的依赖,或者直接用原生 JS 来开发,分享出去是 MPA 的文章页面,这样可以加快分享出去的打开速度,同时也能减少静态资源服务器的压力,因为如果分享出去的是 SPA 的文章页面,那 SPA 所需的静态资源至少都需要去进行协商请求,当然如果服务配置了强缓存就忽略以上所说。

我们首先根据业务所需,来最终确定构建主体,而我们选择了体验至上的 SPA,并选用 Vue 技术栈。

② 目录结构

其实我们看开源的绝大部分项目中,目录结构都会差不太多,我们可以综合一下来个通用的 src 目录:

src
├── assets          // 资源目录 图片,样式,iconfont
├── components      // 全局通用组件目录
├── config          // 项目配置,拦截器,开关
├── plugins         // 插件相关,生成路由、请求、store 等实例,并挂载 Vue 实例
├── directives      // 拓展指令集合
├── routes          // 路由配置
├── service         // 服务层
├── utils           // 工具类
└── views           // 视图层

③ 通用组件

components 中我们会存放 UI 组件库中的那些常见通用组件了,在项目中直接通过设置别名来使用,如果其他项目需要使用,就发到 npm 上。

结构

// components 简易结构
components
├── dist
├── build
├── src      
    ├── modal
    ├── toast
    └── ...
├── index.js             
└── package.json        

项目中使用

如果想最终编译成 es5,直接在 html 中使用或者部署 CDN 上,在 build 配置简单的打包逻辑,搭配着 package.json 构建 UI组件 的自动化打包发布,最终部署 dist 下的内容,并发布到 npm 上即可。

而我们也可直接使用 es6 的代码:

import 'Components/src/modal'

其他项目使用

假设我们发布的 npm 包bm-ui,并且下载到了本地 npm i bm-ui -S:

修改项目的最外层打包配置,在 rules 里 babel-loaderhappypack 中添加 includenode_modules/bm-ui

// webpack.base.conf
...
    rules: [{
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
    },
    {
        test: /\.js$/,
        loader: 'babel-loader',
        // 这里添加
        include: [resolve('src'), resolve('test'), resolve('node_modules/bm-ui')]
    },{
    ...
    }]
...

然后搭配着 babel-plugin-import 直接在项目中使用即可:

import { modal } from 'bm-ui'

多个组件库

同时有多个组件库的话,又或者有同学专门进行组件开发的话,把 components 内部细分一下,多一个文件分层。

components
├── bm-ui-1 
├── bm-ui-2
└── ...

你的打包配置文件可以放在 components 下,进行统一打包,当然如果要开源出去还是放在对应库下。

④ 全局配置,插件与拦截器

这个点其实会是项目中经常被忽略的,或者说很少聚合到一起,但同时我认为是整个项目中的重要之一,后续会有例子说道。

全局配置,拦截器目录结构

config
├── index.js             // 全局配置/开关
├── interceptors        // 拦截器
    ├── index.js        // 入口文件
    ├── axios.js        // 请求/响应拦截
    ├── router.js       // 路由拦截
    └── ...
└── ...

全局配置

我们在 config/index.js 可能会有如下配置:

// config/index.js

// 当前宿主平台 兼容多平台应该通过一些特定函数来取得
export const HOST_PLATFORM = 'WEB'
// 这个就不多说了
export const NODE_ENV = process.env.NODE_ENV || 'prod'

// 是否强制所有请求访问本地 MOCK,看到这里同学不难猜到,每个请求也可以单独控制是否请求 MOCK
export const AJAX_LOCALLY_ENABLE = false
// 是否开启监控
export const MONITOR_ENABLE = true
// 路由默认配置,路由表并不从此注入
export const ROUTER_DEFAULT_CONFIG = {
    waitForData: true,
    transitionOnLoad: true
}

// axios 默认配置
export const AXIOS_DEFAULT_CONFIG = {
    timeout: 20000,
    maxContentLength: 2000,
    headers: {}
}

// vuex 默认配置
export const VUEX_DEFAULT_CONFIG = {
    strict: process.env.NODE_ENV !== 'production'
}

// API 默认配置
export const API_DEFAULT_CONFIG = {
    mockBaseURL: '',
    mock: true,
    debug: false,
    sep: '/'
}

// CONST 默认配置
export const CONST_DEFAULT_CONFIG = {
    sep: '/'
}

// 还有一些业务相关的配置
// ...


// 还有一些方便开发的配置
export const CONSOLE_REQUEST_ENABLE = true      // 开启请求参数打印
export const CONSOLE_RESPONSE_ENABLE = true     // 开启响应参数打印
export const CONSOLE_MONITOR_ENABLE = true      // 监控记录打印

可以看出这里汇集了项目中所有用到的配置,下面我们在 plugins 中实例化插件,注入对应配置,目录如下:

插件目录结构

plugins
├── api.js              // 服务层 api 插件
├── axios.js            // 请求实例插件
├── const.js            // 服务层 const 插件
├── store.js            // vuex 实例插件
├── inject.js           // 注入 Vue 原型插件
└── router.js           // 路由实例插件

实例化插件并注入配置

这里先举出两个例子,看我们是如何注入配置,拦截器并实例化的

实例化 router

import Vue from 'vue'
import Router from 'vue-router'
import ROUTES from 'Routes'
import {ROUTER_DEFAULT_CONFIG} from 'Config/index'
import {routerBeforeEachFunc} from 'Config/interceptors/router'

Vue.use(Router)

// 注入默认配置和路由表
let routerInstance = new Router({
    ...ROUTER_DEFAULT_CONFIG,
    routes: ROUTES
})
// 注入拦截器
routerInstance.beforeEach(routerBeforeEachFunc)

export default routerInstance

实例化 axios

import axios from 'axios'
import {AXIOS_DEFAULT_CONFIG} from 'Config/index'
import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from 'Config/interceptors/axios'

let axiosInstance = {}

axiosInstance = axios.create(AXIOS_DEFAULT_CONFIG)

// 注入请求拦截
axiosInstance
    .interceptors.request.use(requestSuccessFunc, requestFailFunc)
// 注入响应拦截
axiosInstance
    .interceptors.response.use(responseSuccessFunc, responseFailFunc)

export default axiosInstance

我们在 main.js 注入插件

// main.js
import Vue from 'vue'

GLOBAL.vbus = new Vue()

// import 'Components'// 全局组件注册
import 'Directives' // 指令

// 引入插件
import router from 'Plugins/router'
import inject from 'Plugins/inject'
import store from 'Plugins/store'
// 引入组件库及其组件库样式 
// 不需要配置的库就在这里引入 
// 如果需要配置都放入 plugin 即可
import VueOnsen from 'vue-onsenui'
import 'onsenui/css/onsenui.css'
import 'onsenui/css/onsen-css-components.css'
// 引入根组件
import App from './App'

Vue.use(inject)
Vue.use(VueOnsen)

// render
new Vue({
    el: '#app',
    router,
    store,
    template: '<App/>',
    components: { App }
})

axios 实例我们并没有直接引用,相信你也猜到他是通过 inject 插件引用的,我们看下 inject

import axios from './axios'
import api from './api'
import consts from './const'
GLOBAL.ajax = axios
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        Vue.prototype.$ajax = axios
        Vue.prototype.$const = consts
        // 需要挂载的都放在这里
    }
}

这里可以挂载你想在业务中( vue 实例中)便捷访问的 api,除了 $ajax 之外,apiconst 两个插件是我们服务层中主要的功能,后续会介绍,这样我们插件流程大致运转起来,下面写对应拦截器的方法。

请求,路由拦截器

ajax 拦截器中(config/interceptors/axios.js):

// config/interceptors/axios.js

import {CONSOLE_REQUEST_ENABLE, CONSOLE_RESPONSE_ENABLE} from '../index.js'

export function requestSuccessFunc (requestObj) {
    CONSOLE_REQUEST_ENABLE && console.info('requestInterceptorFunc', `url: ${requestObj.url}`, requestObj)
    // 自定义请求拦截逻辑,可以处理权限,请求发送监控等
    // ...
    
    return requestObj
}

export function requestFailFunc (requestError) {
    // 自定义发送请求失败逻辑,断网,请求发送监控等
    // ...
    
    return Promise.reject(requestError);
}

export function responseSuccessFunc (responseObj) {
    // 自定义响应成功逻辑,全局拦截接口,根据不同业务做不同处理,响应成功监控等
    // ...
    // 假设我们请求体为
    // {
    // code: 1010,
    // msg: 'this is a msg',
    // data: null
    // }
    
    let resData =  responseObj.data
    let {code} = resData
    
    switch(code) {
        case 0: // 如果业务成功,直接进成功回调 
            return resData.data;
        case 1111: 
            // 如果业务失败,根据不同 code 做不同处理
            // 比如最常见的授权过期跳登录
            // 特定弹窗
            // 跳转特定页面等
            
            location.href = xxx // 这里的路径也可以放到全局配置里
            return;
        default:
            // 业务中还会有一些特殊 code 逻辑,我们可以在这里做统一处理,也可以下方它们到业务层
            !responseObj.config.noShowDefaultError && GLOBAL.vbus.$emit('global.$dialog.show', resData.msg);
            return Promise.reject(resData);
    }
}

export function responseFailFunc (responseError) {
    // 响应失败,可根据 responseError.message 和 responseError.response.status 来做监控处理
    // ...
    return Promise.reject(responseError);
}

定义路由拦截器(config/interceptors/router.js):

// config/interceptors/router.js

export function routerBeforeFunc (to, from, next) {
    // 这里可以做页面拦截,很多后台系统中也非常喜欢在这里面做权限处理
    
    // next(...)
}

最后在入口文件(config/interceptors/index.js)中引入并暴露出来即可:

import {requestSuccessFunc, requestFailFunc, responseSuccessFunc, responseFailFunc} from './ajax'
import {routerBeforeEachFunc} from './router'

let interceptors = {
    requestSuccessFunc,
    requestFailFunc,
    responseSuccessFunc,
    responseFailFunc,
    routerBeforeEachFunc
}

export default interceptors

请求拦截这里代码都很简单,对于 responseSuccessFunc 中 switch default 逻辑做下简单说明:

  1. responseObj.config.noShowDefaultError 这里可能不太好理解 我们在请求的时候,可以传入一个 axios 中并没有意义的 noShowDefaultError 参数为我们业务所用,当值为 false 或者不存在时,我们会触发全局事件 global.dialog.showglobal.dialog.show我们会注册在 app.vue 中:
// app.vue

export default {
    ...
    created() {
        this.bindEvents
    },
    methods: {
        bindEvents() {
            GLOBAL.vbus.$on('global.dialog.show', (msg) => {
                if(msg) return
                // 我们会在这里注册全局需要操控试图层的事件,方便在非业务代码中通过发布订阅调用
                this.$dialog.popup({
                    content: msg 
                });
            })
        }
        ...
    }
}

这里也可以把弹窗状态放入 Store 中,按团队喜好,我们习惯把公共的涉及视图逻辑的公共状态在这里注册,和业务区分开来

  1. GLOBAL 是我们挂载 window 上的全局对象,我们把需要挂载的东西都放在 window.GLOBAL 里,减少命名空间冲突的可能。
  2. vbus 其实就是我们开始 new Vue() 挂载上去的
GLOBAL.vbus = new Vue()
  1. 我们在这里 Promise.reject 出去,我们就可以在 error 回调里面只处理我们的业务逻辑,而其他如断网超时服务器出错等均通过拦截器进行统一处理。

拦截器处理前后对比

对比下处理前后在业务中的发送请求的代码

拦截器处理前

this.$axios.get('test_url').then(({code, data}) => {
    if( code === 0 ) {
        // 业务成功
    } else if () {}
        // em... 各种业务不成功处理,如果遇到通用的处理,还需要抽离出来
    
    
}, error => {
   // 需要根据 error 做各种抽离好的处理逻辑,断网,超时等等...
})

拦截器处理后

// 业务失败走默认弹窗逻辑的情况
this.$axios.get('test_url').then(({data}) => {
    // 业务成功,直接操作 data 即可
})

// 业务失败自定义
this.$axios.get('test_url', {
    noShowDefaultError: true // 可选
}).then(({data}) => {
    // 业务成功,直接操作 data 即可
    
}, (code, msg) => {
    // 当有特定 code 需要特殊处理,传入 noShowDefaultError:true,在这个回调处理就行
})

为什么要如此配置与拦截器?

在应对项目开发过程中需求的不可预见性时,让我们能处理的更快更好

到这里很多同学会觉得,就这么简单的引入判断,可有可无, 就如我们最近做的一个需求来说,我们 ToC 端项目之前一直是在微信公众号中打开的,而我们需要在小程序中通过 webview 打开大部分流程,而我们也没有时间,没有空间在小程序中重写近 100 + 的页面流程,这是我们开发之初并没有想到的。这时候必须把项目兼容到小程序端,在兼容过程中可能需要解决以下问题:

  1. 请求路径完全不同。
  2. 需要兼容两套不同的权限系统。
  3. 有些流程在小程序端需要做改动,跳转到特定页面。
  4. 有些公众号的 api ,在小程序中无用,需要调用小程序的逻辑,需要做兼容。
  5. 很多也页面上的元素,小程序端不做展示等。

可以看出,稍微不慎,会影响公众号现有逻辑。

  • 添加请求拦截 interceptors/minaAjax.jsinterceptors/minaRouter.js,原有的换更为 interceptors/officalAjax.jsinterceptors/officalRouter.js,在入口文件interceptors/index.js根据当前宿主平台,也就是全局配置 HOST_PLATFORM,通过代理模式策略模式,注入对应平台的拦截器minaAjax.js中重写请求路径和权限处理,在 minaRouter.js 中添加页面拦截配置,跳转到特定页面,这样一并解决了上面的问题 1,2,3
  • 问题 4 其实也比较好处理了,拷贝需要兼容 api 的页面,重写里面的逻辑,通过路由拦截器一并做跳转处理
  • 问题 5 也很简单,拓展两个自定义指令 v-mina-show 和 v-mina-hide ,在展示不同步的地方可以直接使用指令。

最终用最少的代码,最快的时间完美上线,丝毫没影响到现有 toC 端业务,而且这样把所有兼容逻辑绝大部分聚合到了一起,方便二次拓展和修改。

虽然这只是根据自身业务结合来说明,可能没什么说服力,不过不难看出全局配置/拦截器 虽然代码不多,但却是整个项目的核心之一,我们可以在里面做更多 awesome 的事情。

⑤ 路由配置与懒加载

directives 里面没什么可说的,不过很多难题都可以通过他来解决,要时刻记住,我们可以再指令里面操作虚拟 DOM。

路由配置

而我们根据自己的业务性质,最终根据业务流程来拆分配置:

routes
├── index.js            // 入口文件
├── common.js           // 公共路由,登录,提示页等
├── account.js          // 账户流程
├── register.js         // 挂号流程
└── ...

最终通过 index.js 暴露出去给 plugins/router 实例使用,这里的拆分配置有两个注意的地方:

  • 需要根据自己业务性质来决定,有的项目可能适合业务线划分,有的项目更适合以 功能 划分。
  • 在多人协作过程中,尽可能避免冲突,或者减少冲突。

懒加载

文章开头说到单页面静态资源过大,首次打开/每次版本升级后都会较慢,可以用懒加载来拆分静态资源,减少白屏时间,但开头也说到懒加载也有待商榷的地方:

  • 如果异步加载较多的组件,会给静态资源服务器/ CDN 带来更大的访问压力的同时,如果当多个异步组件都被修改,造成版本号的变动,发布的时候会大大增加 CDN 被击穿的风险。
  • 懒加载首次加载未被缓存的异步组件白屏的问题,造成用户体验不好。
  • 异步加载通用组件,会在页面可能会在网络延时的情况下参差不齐的展示出来等。

这就需要我们根据项目情况在空间和时间上做一些权衡。

以下几点可以作为简单的参考:

  • 对于访问量可控的项目,如公司后台管理系统中,可以以操作 view 为单位进行异步加载,通用组件全部同步加载的方式。
  • 对于一些复杂度较高,实时度较高的应用类型,可采用按功能模块拆分进行异步组件加载。
  • 如果项目想保证比较高的完整性和体验,迭代频率可控,不太关心首次加载时间的话,可按需使用异步加载或者直接不使用。

打包出来的 main.js 的大小,绝大部分都是在路由中引入的并注册的视图组件。

⑥ Service 服务层

服务层作为项目中的另一个核心之一,“自古以来”都是大家比较关心的地方。

不知道你是否看到过如下组织代码方式:

views/
    pay/
        index.vue
        service.js
        components/
            a.vue
            b.vue

service.js 中写入编写数据来源

export const CONFIAG = {
    apple: '苹果',
    banana: '香蕉'
}
// ...

// ① 处理业务逻辑,还弹窗
export function getBInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    }).then({age} => {
        this.$modal.show({
            content: age
        })
    })
}

// ② 不处理业务,仅仅写请求方法
export function getAInfo ({name = '', id = ''}) {
    return this.$ajax.get('/api/info', {
        name, 
        id
    })
}

...

简单分析:

  • ① 就不多说了,拆分的不够单纯,当做二次开发的时候,你还得去找这弹窗到底哪里出来的。
  • ② 看起来很美好,不掺杂业务逻辑,但不知道你与没遇到过这样情况,经常会有其他业务需要用到一样的枚举,请求一样的接口,而开发其他业务的同学并不知道你在这里有一份数据源,最终造成的结果就是数据源的代码到处冗余

我相信②在绝大多数项目中都能看到。

那么我们的目的就很明显了,解决冗余,方便使用,我们把枚举和请求接口的方法,通过插件,挂载到一个大对象上,注入 Vue 原型,方面业务使用即可。

目录层级(仅供参考)

service
├── api
    ├── index.js             // 入口文件
    ├── order.js             // 订单相关接口配置
    └── ...
├── const                   
    ├── index.js             // 入口文件
    ├── order.js             // 订单常量接口配置
    └── ...
├── store                    // vuex 状态管理
├── expands                  // 拓展
    ├── monitor.js           // 监控
    ├── beacon.js            // 打点
    ├── localstorage.js      // 本地存储
    └── ...                  // 按需拓展
└── ...

抽离模型

首先抽离请求接口模型,可按照领域模型抽离 (service/api/index.js):

{
    user: [{
        name: 'info',
        method: 'GET',
        desc: '测试接口1',
        path: '/api/info',
        mockPath: '/api/info',
        params: {
            a: 1,
            b: 2
        }
    }, {
        name: 'info2',
        method: 'GET',
        desc: '测试接口2',
        path: '/api/info2',
        mockPath: '/api/info2',
        params: {
            a: 1,
            b: 2,
            b: 3
        }
    }],
    order: [{
        name: 'change',
        method: 'POST',
        desc: '订单变更',
        path: '/api/order/change',
        mockPath: '/api/order/change',
        params: {
            type: 'SUCCESS'
        }
    }]
    ...
}

定制下需要的几个功能:

  • 请求参数自动截取。
  • 请求参数不传,则发送默认配置参数。
  • 得需要命名空间。
  • 通过全局配置开启调试模式。
  • 通过全局配置来控制走本地 mock 还是线上接口等。

插件编写

定制好功能,开始编写简单的 plugins/api.js 插件:

import axios from './axios'
import _pick from 'lodash/pick'
import _assign from 'lodash/assign'
import _isEmpty from 'lodash/isEmpty'

import { assert } from 'Utils/tools'
import { API_DEFAULT_CONFIG } from 'Config'
import API_CONFIG from 'Service/api'


class MakeApi {
    constructor(options) {
        this.api = {}
        this.apiBuilder(options)
    }


    apiBuilder({
    	sep = '|',
    	config = {},
    	mock = false, 
    	debug = false,
    	mockBaseURL = ''
    }) {
    	Object.keys(config).map(namespace => {
    		this._apiSingleBuilder({
                namespace, 
                mock, 
                mockBaseURL, 
                sep, 
                debug, 
                config: config[namespace]
            })
    	})
    }
    _apiSingleBuilder({
    	namespace, 
    	sep = '|',
    	config = {},
    	mock = false, 
    	debug = false,
    	mockBaseURL = ''
    }) {
        config.forEach( api => {
            const {name, desc, params, method, path, mockPath } = api
            let apiname = `${namespace}${sep}${name}`,// 命名空间
                url = mock ? mockPath : path,//控制走 mock 还是线上
                baseURL = mock && mockBaseURL
            
            // 通过全局配置开启调试模式。
            debug && console.info(`调用服务层接口${apiname},接口描述为${desc}`)
            debug && assert(name, `${apiUrl} :接口name属性不能为空`)
            debug && assert(apiUrl.indexOf('/') === 0, `${apiUrl} :接口路径path,首字符应为/`)

            Object.defineProperty(this.api, `${namespace}${sep}${name}`, {
                value(outerParams, outerOptions) {
                
                    // 请求参数自动截取。
                    // 请求参数不穿则发送默认配置参数。
                    let _data = _isEmpty(outerParams) ? params : _pick(_assign({}, params, outerParams), Object.keys(params))
                    return axios(_normoalize(_assign({
                        url,
                        desc,
                        baseURL,
                        method
                    }, outerOptions), _data))
                }
            })      
        })
    }       
}

function _normoalize(options, data) {
    // 这里可以做大小写转换,也可以做其他类型 RESTFUl 的兼容
    if (options.method === 'POST') {
        options.data = data
    } else if (options.method === 'GET') {
        options.params = data
    }
    return options
} 
// 注入模型和全局配置,并暴露出去
export default new MakeApi({
	config: API_CONFIG,
	...API_DEFAULT_CONFIG
})['api']

挂载到 Vue 原型上,上文有说到,通过 plugins/inject.js

import api from './api'
 
export default {
    install: (Vue, options) => {
        Vue.prototype.$api = api
        // 需要挂载的都放在这里
    }
}

使用

这样我们可以在业务中愉快的使用业务层代码:

// .vue 中
export default {
    methods: {
        test() {
            this.$api['order/info']({
                a: 1,
                b: 2
            })
        }
    }
}

即使在业务之外也可以使用:

import api from 'Plugins/api'

api['order/info']({
    a: 1,
    b: 2
})

当然对于运行效率要求高的项目中,避免内存使用率过大,我们把命名空间支持成驼峰,直接用解构的方式引入使用,最终利用 webpacktree-shaking 减少打包体积即可。

import {orderInfo as getOrderInfo} from 'Plugins/api'

getOrderInfo({
    a: 1,
    b: 2
})

一般来说,多人协作时候大家都可以先看 api 是否有对应接口,当业务量上来的时候,也肯定会有人出现找不到,或者找起来比较费劲,这时候我们完全可以在 请求拦截器中,把当前请求的 urlapi 中的请求做下判断,如果有重复接口请求路径,则提醒开发者已经配置相关请求,根据情况是否进行二次配置即可。

最终我们可以拓展 Service 层的各个功能:

基础

  • api异步与后端交互
  • const常量枚举
  • storeVuex 状态管理

拓展

  • localStorage:本地数据,稍微封装下,支持存取对象即可
  • monitor监控功能,自定义搜集策略,调用 api 中的接口发送
  • beacon打点功能,自定义搜集策略,调用 api 中的接口发送

constlocalStoragemonitorbeacon 根据业务自行拓展暴露给业务使用即可,思想也是一样的,下面着重说下 store(Vuex)

插一句:如果看到这里没感觉不妥的话,想想上面 plugins/api.js 有没有用单例模式?该不该用?

⑦ 状态管理与视图拆分

Vuex 源码分析可以看我之前写的文章

我们是不是真的需要状态管理?

答案是否定的,就算你的项目达到 10 万行代码,那也并不意味着你必须使用 Vuex,应该由业务场景决定。

业务场景

  1. 第一类项目:业务/视图复杂度不高,不建议使用 Vuex,会带来开发与维护的成本,使用简单的 vbus 做好命名空间,来解耦即可。
let vbus = new Vue()
vbus.$on('print.hello', () => {
    console.log('hello')
})

vbus.$emit('print.hello')
  1. 第二类项目:类似多人协作项目管理有道云笔记网易云音乐微信网页版/桌面版应用,功能集中,空间利用率高,实时交互的项目,无疑 Vuex 是较好的选择。这类应用中我们可以直接抽离业务领域模型
store
├── index.js          
├── actions.js        // 根级别 action
├── mutations.js      // 根级别 mutation
└── modules
    ├── user.js       // 用户模块
    ├── products.js   // 产品模块
    ├── order.js      // 订单模块
    └── ...

当然对于这类项目,vuex 或许不是最好的选择,有兴趣的同学可以学习下 rxjs

  1. 第三类项目:后台系统或者页面之间业务耦合不高的项目,这类项目是占比应该是很大的,我们思考下这类项目:

全局共享状态不多,但是难免在某个模块中会有复杂度较高的功能(客服系统,实时聊天,多人协作功能等),这时候如果为了项目的可管理性,我们也在 store 中进行管理,随着项目的迭代我们不难遇到这样的情况:

store/
    ...
    modules/
        b.js
        ...
views/
    ...
    a/
        b.js
        ...
        
  • 试想下有几十个 module,对应这边上百个业务模块,开发者在两个平级目录之间调试与开发的成本是巨大的。
  • 这些 module 可以在项目中任一一个地方被访问,但往往他们都是冗余的,除了引用的功能模块之外,基本不会再有其他模块引用他。
  • 项目的可维护程度会随着项目增大而增大。

如何解决第三类项目的 store 使用问题?

先梳理我们的目标:

  • 项目中模块可以自定决定是否使用 Vuex。(渐进增强)
  • 从有状态管理的模块,跳转没有的模块,我们不想把之前的状态挂载到 store 上,想提高运行效率。(冗余)
  • 让这类项目的状态管理变的更加可维护。(开发成本/沟通成本)

实现

我们借助 Vuex 提供的 registerModuleunregisterModule 一并解决这些问题,我们在 service/store 中放入全局共享的状态:

service/
    store/
        index.js
        actions.js
        mutations.js
        getters.js
        state.js

一般这类项目全局状态不多,如果多了拆分 module 即可。

编写插件生成 store 实例

import Vue from 'vue'
import Vuex from 'vuex'
import {VUEX_DEFAULT_CONFIG} from 'Config'
import commonStore from 'Service/store'

Vue.use(Vuex)

export default new Vuex.Store({
    ...commonStore,
    ...VUEX_DEFAULT_CONFIG
})

对一个需要状态管理页面或者模块进行分层:

views/
    pageA/
        index.vue
        components/
            a.vue
            b.vue
            ...
        children/
            childrenA.vue
            childrenB.vue
            ...
        store/
            index.js
            actions.js
            moduleA.js  
            moduleB.js

module 中直接包含了 gettersmutationsstate,我们在 store/index.js 中做文章:

import Store from 'Plugins/store'
import actions from './actions.js'
import moduleA from './moduleA.js'
import moduleB from './moduleB.js'

export default {
    install() {
        Store.registerModule(['pageA'], {
            actions,
            modules: {
                moduleA,
                moduleB
            },
            namespaced: true
        })
    },
    uninstall() {
        Store.unregisterModule(['pageA'])
    }
}

最终在 index.vue 中引入使用, 在页面跳转之前注册这些状态和管理状态的规则,在路由离开之前,先卸载这些状态和管理状态的规则

import store from './store'
import {mapGetters} from 'vuex'
export default {
    computed: {
        ...mapGetters('pageA', ['aaa', 'bbb', 'ccc'])
    },
    beforeRouterEnter(to, from, next) {
        store.install()
        next()
    },
    beforeRouterLeave(to, from, next) {
        store.uninstall()
        next()
    }
}

当然如果你的状态要共享到全局,就不执行 uninstall

这样就解决了开头的三个问题,不同开发者在开发页面的时候,可以根据页面特性,渐进增强的选择某种开发形式。

其他

这里简单列举下其他方面,需要自行根据项目深入和使用。

打包,构建

这里网上已经有很多优化方法:dllhappypack多线程打包等,但随着项目的代码量级,每次 dev 保存的时候编译的速度也是会愈来愈慢的,而一过慢的时候我们就不得不进行拆分,这是肯定的,而在拆分之前尽可能容纳更多的可维护的代码,有几个可以尝试和规避的点:

  1. 优化项目流程:这个点看起来好像没什么用,但改变却是最直观的,页面/业务上的化简为繁会直接体现到代码上,同时也会增大项目的可维护,可拓展性等。
  2. 减少项目文件层级纵向深度。
  3. 减少无用业务代码,避免使用无用或者过大依赖(类似 moment.js 这样的库)等。

样式

  • 尽可能抽离各个模块,让整个样式底层更加灵活,同时也应该尽可能的减少冗余。
  • 如果使用的 sass 的话,善用 %placeholder 减少无用代码打包进来。

MPA 应用中样式冗余过大,%placeholder 也会给你带来帮助。

Mock

很多大公司都有自己的 mock 平台,当前后端定好接口格式,放入生成对应 mock api,如果没有 mock 平台,那就找相对好用的工具如 json-server 等。

代码规范

请强制使用 eslint,挂在 git 的钩子上。定期 diff 代码,定期培训等。

TypeScript

非常建议用 TS 编写项目,可能写 .vue 有些别扭,这样前端的大部分错误在编译时解决,同时也能提高浏览器运行时效率,可能减少 re-optimize 阶段时间等。

测试

这也是项目非常重要的一点,如果你的项目还未使用一些测试工具,请尽快接入,这里不过多赘述。

拆分系统

当项目到达到一定业务量级时,由于项目中的模块过多,新同学维护成本,开发成本都会直线上升,不得不拆分项目,后续会分享出来我们 ToB 项目在拆分系统中的简单实践。

最后

时下有各种成熟的方案,这里只是一个简单的构建分享,里面依赖的版本都是我们稳定下来的版本,需要根据自己实际情况进行升级。

项目底层构建往往会成为前端忽略的地方,我们既要从一个大局观来看待一个项目或者整条业务线,又要对每一行代码精益求精,对开发体验不断优化,慢慢累积后才能更好的应对未知的变化。

西班牙斗牛士,再见

作为伪球迷,我也曾目睹过西班牙斗牛士军团在2010年拿下大力神杯,我记得那时的感动不亚于深夜里见证费德勒的又一 […]

作为伪球迷,我也曾目睹过西班牙斗牛士军团在2010年拿下大力神杯,我记得那时的感动不亚于深夜里见证费德勒的又一次喜极而泣。

随后的两届世界杯,我依旧追随着它的步伐。如果说上届世界杯,西班牙难逃卫冕冠军魔咒,那么昨晚八分之一决赛面对东道主,控球但不能控场的画面,一定不是我所熟悉的感觉,折戟沉沙并不可怕,但可怕的是自我感觉良好。就我的感觉而言,Tiki-Taka战术并未过时,传控不应该是催眠所有人包括自己,美丽足球的“美”也不应该这样诠释。

我还记得那支哈白巴制造的梦幻中场,托雷斯和比利亚双子星为这战术画下句点的瞬间。如今他们都已凋零,希望我有生之年能看到另一支让我陶醉的西班牙斗牛士军团。

再见,🇪🇸

emoji功能上线!

折腾了近两个小时,最后还是放弃了在function.php里插入代码的方式,改用第三方插件再加上样式优化。 不 […]

折腾了近两个小时,最后还是放弃了在function.php里插入代码的方式,改用第三方插件再加上样式优化。

不知道是我哪一环节出错,还是网上的教程已经落后于WP的版本迭代。

文章撰写区和评论区已支持emoji,😳😜😍

顺便应景世界杯,支持下西班牙🇪🇸