一些Web最佳实践的探索

Posted by Yinode on Tuesday, December 18, 2018

TOC

最近用 Django 整了一个匿名的论坛,包含主题回复点赞,板块,用户等系统,麻雀虽小,但是基本功能还是全的的。但是不对接多可惜啊。

正好这半年多的前端开发经验让我获得了一些最佳实践的灵感,所以正好乘这次机会,干脆写一个不受任何人,不受业务影响的情况下,我心目中的好项目。我想  把所有我新学习到,理解到的,想做的,觉得对项目有优势的东西,进行一个整合,我不希望在这个项目上有任何的业务妥协。

主要围绕了以下几点进行

  •  明确 干净 统一的目录系统
  • 请求拦截器的进阶使用
  • RESTFul 风格的接口
  • Views + components + base 的组件化系统
  • 前端预渲染
  • 打包优化
  •  更加优雅便捷的部署到 Gh-pages
  • docker 虚拟化
  • Vue 组件内部可读性
  •  后端给签名 前端直接上传到阿里云
  • 更好的去利用 Stylus 的变量
  • PWA 的使用
  • 全面拥抱 Asyncawait 函数

 接下来我会对我做到的一些事情进行更加详细的解释

目录结构

.
├── babel.config.js
├── dist
├── git-pages-deploy.sh
├── package.json
├── public
├── src
│   ├── App.vue
│   ├── api
│   │   ├── request.js
│   │   └── urls.js
│   ├── assets
│   ├── base
│   │   └── shortcut-scroll
│   │       └── shortcut-scroll.vue
│   ├── common
│   │   ├── config
│   │   │   └── config.js
│   │   ├── mixins
│   │   │   └── mixins.js
│   │   └── stylus
│   │       ├── common.styl
│   │       └── variable.styl
│   ├── components
│   │   ├── header
│   │   │   └── header.vue
│   │   ├── plate-nav
│   │   │   └── plate-nav.vue
│   │   ├── reply-box
│   │   │   └── reply-box.vue
│   │   ├── reply-item
│   │   │   └── reply-item.vue
│   │   ├── reply-view
│   │   │   └── reply-view.vue
│   │   ├── theme-item
│   │   │   └── theme-item.vue
│   │   └── theme-view
│   │       └── theme-view.vue
│   ├── main.js
│   ├── registerServiceWorker.js
│   ├── router
│   │   └── index.js
│   └── views
│       ├── create-theme
│       │   └── create-theme.vue
│       ├── home
│       │   └── home.vue
│       ├── layout.vue
│       ├── login.vue
│       ├── search
│       │   └── search.vue
│       └── theme
│           └── theme.vue
├── vue.config.js
├── yarn-error.log
└── yarn.lock

 这一部分大家的习惯并不一样,但应该做到易于理解,避免特别深度的嵌套,尽可能的扁平化

拦截器

 请求拦截器其实已经用的非常普遍了,这个东西是顺应了 AOP,面向切面编程的思想。我刚进入工作的时候,其实自己写了一个拦截器,但是没了解到 axios 的拦截器,所以重复造了一个不太好用的轮子,说来有点惭愧

这一次,我进一步的使用了 axios 的拦截系统,让他做到了如下功能

  1. 当服务器发送错误,或者请求不被允许时,自动调用 Message 组件 来告知用户的错误信息
  2.  登录拦截器,当请求必须登录时,自动调整到登录页(这个其实非常普遍了)
  3. 自动重发请求(最大次数我设置为 3 次)

 通过这样的一个系统,我能够极大的减轻判断接口错误与否 登录与否等情况检查的负担。让我的注意力能够放到应用的核心上

import axios from 'axios'
import { Message } from 'element-ui'
import Router from '@/router'

const baseURL =
  process.env.NODE_ENV === 'development'
    ? 'https://api.yinode.tech/anode/'
    : 'https://api.yinode.tech/anode/'

const request = axios.create({
  baseURL,
  timeOut: 10000,
  reponseType: 'json'
})

// token检查
request.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.token = token
  }
  return config
})

request.defaults.retry = 2
request.defaults.retryDelay = 3000
// 错误码检查
request.interceptors.response.use(
  res => {
    if (res.status === 200) {
      return res.data
    }
    // TOKEN失效 重新登录
    if (res.status === 401) {
      Router.push({ name: 'login' })
    } else {
      // 其他错误
      Message({
        type: 'error',
        message: res.data.msg
      })
      return Promise.reject(res.data.msg)
    }
  },
  err => {
    var config = err.config
    if (!config || !config.retry) return Promise.reject(err)
    config.__retryCount = config.__retryCount || 0

    if (config.__retryCount >= config.retry) {
      Message({
        type: 'error',
        message: '重复请求次数已达上限,停止请求'
      })
      return Promise.reject(err)
    }
    config.__retryCount += 1

    var backoff = new Promise(resolve => {
      setTimeout(() => {
        resolve()
      }, config.retryDelay || 1)
    })
    Message({
      type: 'error',
      message: `${(err.response &&
        err.response.data &&
        err.response.data.msg) ||
        '哎呀,请求出错了'},正在尝试重新请求`
    })
    return backoff.then(() => {
      return request(config)
    })
  }
)

export default request

RESTFul

这个东西其实需要后端配合,我觉得这个东西还是非常棒的,他充分利用了 HTTP 自身本来就很高的语义化,让整个请求过程变得更加易读易理解。

我主要用了 Django + drf 这个框架写了整个后端系统。作为一个后端菜鸟,我  还是想谈谈我对 Django 的感觉。

  • ORM 很强大 但让我感觉不透明 有点距离感
  • django admin 很牛逼
  • 写起来感觉会有点笨重
  • 整体开发效率还是非常不错的。特别是用 ViewSet 来进行 API Path 的自动构建

tips: 其实我们公司的 CODE 依旧是放到  响应结果的 JSON 里面的,但我这次直接利用的 HTTP 的 Status CODE 我觉得这更好 

在前端部分我对于路由也尽可能的去贴近 RESTFul 的风格,比如查看主题详情

/app/theme/47

我放弃了用 query 传递 ID 的丑陋方式,完全拥抱更加符合资源定位的 URL

组件化系统

更加明确了整个组件化的概念

Views 是整个系统的核心骨架

── views
│       ├── create-theme
│       │   └── create-theme.vue
│       ├── home
│       │   └── home.vue
│       ├── layout.vue
│       ├── login.vue
│       ├── search
│       │   └── search.vue
│       └── theme
│           └── theme.vue

 我的顶级 Views 其实只有两个,一个是应用的通用布局页面 layout(里面包含了 Header 这样的 components),另外一个是登录组件。

而存活于 layout 组件的 router-view 中的 二级 Views 则被我封装到了同级的文件夹中。

components 应该是可以被  通用的,被复用,拥有一定业务逻辑(少量)的组件

base 应该是没有明确业务逻辑,简单化的小型组件

当然 components 以及 base 类型的组件都应该控制里面的代码量,避免一个组件过于复杂,如果发现代码过多阅读困难,应进行拆分,我目前是控制在 200 行以内, 大部分在 100 行左右。

前端预渲染

所谓  预渲染,其实就是在打包完成后,开启这个服务,并通过无头浏览器(没有 GUI)抓取里面的 HTML 结构(具体在页面加载到什么时候开始抓, 可以由你来确定),并输出到你最终打包结果的 HTML 中去。

我这里用了一个 webpack 的 plugin

由于我用了 vue-cli3 所以可以更方便的添加插件

vue add prerender-spa

接下来在你的项目目录下创建一个.prerender-spa.json 文件

{
  "renderRoutes": ["/"],
  "useRenderEvent": true,
  "headless": true,
  "onlyProduction": true,
  "customRendererConfig": {
    "renderAfterDocumentEvent": "now-pre-render"
  }
}

我目前只需要预渲染根路由,接下来去 Home 组件的 mounted 中触发我的自定义抓取事件,插件就会自动去抓取当前的 HTML 内容。

// home.vue
export default {
  mounted() {
    // Now Pre render
    document.dispatchEvent(new Event('now-pre-render'))
  }
}

最终输出的 Index.html 中就会有你触发自定义时间时的结构。

通常  你抓取的时机就是你的整个页面骨架已经出来的时候。

打包优化

这里我遇到了几个问题

elementui 全量打包容量太大

解决办法:按需打包

在 babel.confis.js 中 设置如下内容

module.exports = {
  presets: ['@vue/app'],
  plugins: [
    [
      'component',
      { libraryName: 'element-ui', styleLibraryName: 'theme-chalk' }
    ]
  ]
}

并安装 babel-plugin-component 这个插件

接着不要直接引入整个 Element-ui ,用了什么,引入什么。

import { Button, Select, Upload, Loading, Form } from 'element-ui'

Vue.use(Form)
// ... more

moment.js 太大

解决方法:删除不必要的本地化时间格式化

利用 Vue-cli webpack 插件暴露手段,注册 Webpack 的过滤组件

// vue.config.js
import webpack from 'webpack'
module.exports = {
  baseUrl: process.env.NODE_ENV === 'production' ? '/ANode-forum/' : '/',
  chainWebpack: config => {
    config
      .plugin('ignore')
      .use(new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/))
  }
}

Gh-pages

我其实相当喜欢这个东西,用来放 Demo SPA 的前端部分都非常合适,这里从 Vue-cli3 的文档中偷学了一招(本来是利用的切换  分支的方式,操作麻烦,而且比较蠢)

#!/usr/bin/env sh

# abort on errors
set -e

# build
npm run build

# navigate into the build output directory
cd dist

echo 'yinode.tech' > CNAME

git init
git add -A
git commit -m 'deploy'

# if you are deploying to https://<USERNAME>.github.io
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master

# if you are deploying to https://<USERNAME>.github.io/<REPO>
git push -f git@github.com:zhangzhengyi12/ANode-forum.git master:gh-pages

cd -

运行一下这个脚本就自动  打包部署了,同理其他的项目很多都可以利用这个东西。关键是利用了重新 init,之前怎么没想到呢

docker

其实 python 对于环境的要求比 node.js 高,各种 pip 版本啥的,自带的依赖管理也不是很好用, 部署到服务器上可能会有问题。这里就分享一下我的 Docker 配置

# Dockerfile
FROM python:3
ENV PYTHONUNBUFFERED 1
RUN mkdir /code
WORKDIR /code
ADD requirements.text /code/
RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.text
ADD . /code/

RUN python3 manage.py migrate
version: '3'
services:
  web:
    build: .
    command: python3 manage.py runserver 0.0.0.0:9528
    volumes:
      - .:/code
    ports:
      - '8020:9528'

 自动构建镜像并守护运行

docker-compose up –build -d

持续构建

git pull

docker-compose up -d

代码可读性

多用 Vue 的 computed

避免在 模版中 写 v-if="xxx.length > 0" 这种东西,特别是复杂的判断,一定要写成计算属性 v-if="isEmptyList" 这样可读性就好了非常多。

避免过多的 IF 嵌套

我基本上都是如果函数运行不下去 尽快 return

// good
function name(params) {
  if (!params.xx || !params.bb) {
    return
  }
  // go
}
// bad
function name(params) {
  if (params.xx) {
    if (params.bb) {
      // go
    }
  }
}

 可读性会有提高

多用更容易理解的迭代方式

map filter reduce some every 这些东西实在太棒了,写起来真的超级优美好吗,少用那些 For 循环吧,真的不好看,除非你特别需要性能。

CDN 图片前端直接上传

这里我借鉴了一个 demo

非常好用哦,把他放到你的服务器上 以后你的任何项目都可以独立的去调用,很方便。

Stylus

 其实大家用这些预编译 CSS 的时候,一定要充分利用这里面的系统哦,其实  我觉得好的 CSS 最难的地方就是高度复用性。整个 CSS 的类名就是一个组件,一个东西的样式只要附着一些比较通用的类名,然后稍微加点细节修改,就能拥有样式。并且能够利用权重覆盖系统,做到灵活,更少的  代码量。低耦合。

其实我觉得 CSS 写得好真的很难。

这次我大量用了主题的颜色 以及一些通用的类名。当然我觉得还能做的更好,继续努力 !

PWA

这个东西的缓存能力实在太强了, 也能够优雅的降级,我找不到特别充分不去用的理由啊。

赶紧去装起来吧。

Async/await

很多人说这个可能才是 JS 异步的终极解决方案,真的是超爽,这次我全面使用的 axios 来进行异步操作。

export default {
  async getThemeList() {
    this.onLoadingThemeList = true
    try {
      const res = await this.$axios.get(theme, {
        params: Object.assign({}, this.themeListOptions, {
          title: decodeURIComponent(this.keywords)
        })
      })
    } catch (e) {
      return false
    } finally {
      this.onLoadingThemeList = false
    }
    if (res.data.length <= 0) {
      this.themeListIsEnd = true
    }
    this.themeList = this.themeList.concat(res.data)
    this.themeTotal = res.total
  }
}

我觉得这个是一个比较典型的东西,我觉得是非常优雅的。用了这个之后你的库.js 大约会增加 20kb 我觉得还是非常  划算的。

开启 Loading >  请求 > 错误直接返回 > 关闭 Loading > 成功后端数据处理

总结

以上只是我个人的一些最佳实践的处理,欢迎大家讨论,纠正。共同进步,继续努力。

CODE & Preview

github 预览