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 的拦截系统,让他做到了如下功能
- 当服务器发送错误,或者请求不被允许时,自动调用 Message 组件 来告知用户的错误信息
- 登录拦截器,当请求必须登录时,自动调整到登录页(这个其实非常普遍了)
- 自动重发请求(最大次数我设置为 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 > 成功后端数据处理
总结
以上只是我个人的一些最佳实践的处理,欢迎大家讨论,纠正。共同进步,继续努力。