V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
pkwenda
V2EX  ›  前端开发

《从零构建前后分离的 web 项目》实战 - 欲善其事必先利其器 继续打磨前端架构

  •  
  •   pkwenda ·
    pkwenda · 2018-10-24 02:54:53 +08:00 · 2198 次点击
    这是一个创建于 2217 天前的主题,其中的信息可能已经有所发展或是发生改变。

    工欲善其事必先利其器 - 继续打磨前端架构

    抱歉生病拖更了,1024 快乐

    本文永久更新地址

    填坑

    上回还真的有同学提到了这个问题,感谢细心的你。 @_noob

    其实是没任何问题的,只不过看起来违背了常见的结构,像是有问题。其实是上文为了照顾初学者,怕大家因为麻烦而放弃,并没有一次性改的“看起来那么复杂”,我们来填下坑。

    为了照顾没有实时跟着我连载的同学,每一章的代码单独发布在我的 Github 博客,不进行覆盖更新 [除非代码有错误进行修改],这样避免了 2019 年小明同学望着前两章和 Github 最终版本代码发呆 ( release 也不是特别友好)

    我们把上文的 renderer/src 改成比较容易理解的 src/renderer ,这其实是一个编程习惯问题。

    上文 renderer/src 的问题

    eslint-config-alloy : 想写出更规范的代码可以参考这个规则

    下面进入本章正题


    axios

    使用了 vue 的你,发现 Vue 居然不能发请求,于是你 Google 了下,发现可以用 Vue-Resource。 你去问别人 Vue-Resource 怎么样,他说不要用 Vue-Resource,因为 Vue-Resource 官方已经停止维护了,你应该用 Axios、或者 fetch。但是我们想拥抱 ES6 排除掉了 ES5 的 fetch (当然也有 ES6-fetch ),这里我们使用 Axios !

    Tips

    这里呢也科普一下:什么时候依赖需要放到 dependencies、什么时候依赖需要放到 devDependencies:

    devDependencies:顾名思义,仅在开发( dev )模式下如:webpack..loader、eslint、babel、打包后部署时完全用不到的、仅在开发需要 编译、检测、转换 的放在这里。 dependencies:例如:axios、chart、js-cookie、less、lodash、underscore 等运行时的库或工具类等相关依赖我们要放在这里

    不过基本不用担心,官网都会提供 start 说明,但是我们要大概明白意思,不要机械般的 copy。

    引入 Axios

    • 直接玩最新的

    2018-09-28 截图 npmjs.com

    • 添加依赖
    "dependencies": {    
        "axios": "^0.18.0"
     }
    

    基于上一章内容,别忘了重新 npm i 下载一下

    还记得我们自动生成的 vue 主页面脚本 main.js 吗?

    封装 axios

    我们在 src/renderer/utils 建立一个 request.js 在这个请求脚本中,对 Axios 做一些必要的封装,大概内容是用 拦截器 axios.interceptors 对请求和响应做些拦截,定义一下 API 的前缀,处理一些常见的 HTTP 状态码。

    • interceptors 文档

    我尽可能的为大家写了详细的注释。

    // src/renderer/utils/request.js
    import axios from 'axios'
    
    //这里一般指后端项目 API 的前缀,例如 /baidu/*/*/1.api  /mi/*/*/2.api
    const BASE_API = ""
    
    export function axiosIntercept(Vue, router) {
        const axiosIntercept = axios.create({
            baseURL: BASE_API
        })
    
        //http request 拦截器 一般用来在请求前塞一些全局的配置、或开启一些 css 加载动画
        axios.interceptors.request.use(
            (config) => {
                // 判断是否存在 token,如果存在的话,则每个 http header 都加上 token
                // if (store.getters.accessToken) {
                //     console.log(store.getters.accessToken)
                //     config.headers.Authorization = `token ${store.getters.accessToken}`;
                // }
    
                //todo:加载动画
    
                //若有需求可以处理一下 post 亦或改变 post 传输格式
                if (config.method === 'post') {
    
                };
    
                return config;
            }, function (err) {
                return Promise.reject(err);
            });
    
    
        //http response 拦截器 一般用来根据一些后端协议特殊返回值做一些处理,例如:权限方面、404... 或关闭一些 css 加载动画
        axiosIntercept.interceptors.response.use(function (response) {
            // todo: 暂停加载动画
            return response;
        }, function (err) {
            //捕获异常
            if (err.response) {
                switch (err.response.status) {
                    case 401:
                        // do something 这里我们写完后端做好约束再完善
                }
            }
            return Promise.reject(err);
        });
        return axiosIntercept;
    }
    
    
    

    大家还记得我们用 vue-cli 生成的 vue 主页脚本 main.js 吧,这里我们需要对 Axios 和 Vue 做一个耦合。

    // src/renderer/main.js
    import axios from 'axios'
    import { axiosIntercept } from './utils/request'
    
    // 将 Axios 扩展到 Vue 原型链中
    Vue.prototype.$http = axiosIntercept(Vue)
    

    这样我们在写业务逻辑,直接在 Vue 的上下文中 使用 this.$http 来发送请求。既实现了拦截、又实现了状态的共享。

    知其然,知其所以然

    • 这样做的意义在哪?

    节省代码量,让代码更加易读

    • 为什么?

    扩展到原型链,使 Axios 运行时共享 Vue 原型链的内容,减少了很多指代 Vue 的临时变量

    • 举个栗子

    传统情况

    import axios from 'axios'
    
    
    new Vue({
      data: {
        user: ""
      },
      created: function () {
        //此时作用域在 Vue 上,缓存起来,要依赖此变量
        let _this = this;
        axios.get("/user/getUserInfo/" + userName).then(res => {
                if (res.data.code === 200) {
                    //此时作用域在 axios 上,拿不到 vue 绑定的值,只能借助刚才缓存的_this 上下文
                    _this.data.user = res.data.user
                }
            });
        }
    })
    
    

    代理之后

     
    
    
    new Vue({
      data: {
        user: ""
      },
      created: function () {
        // axios 成为了 vue 的原型链一部分,共享 vue 状态。
        
        this.$http.get("/user/getUserInfo/" + userName).then(res => {
                if (res.data.code === 200) {
                    //注意,axios 回调,应该尽量使用箭头函数,可以继承父类上下文,否则类似闭包,还是无法共享变量、
                    // 更优雅了一些
                    this.data.user = res.data.user
                }
            });
        }
    })
    
    

    不懂 prototype 可以翻翻我以前写的文章

    proxy

    先简单弄一下,为前后分离打个小铺垫

    webPack

    • webPack 的别名
    resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
          '@': resolve('src/renderer'),
        }
      },
    

    为了使用起来更加优雅,可以为每个常用的目录都建立别名

    resolve: {
       extensions: ['.js', '.vue', '.json'],
       alias: {
         'vue$': 'vue/dist/vue.esm.js',
         '@': resolve('src/renderer'),
         'assets': resolve('src/renderer/assets'),
         'components': resolve('src/renderer/components'),
         'container': resolve('src/renderer/container'),
         'utils': resolve('src/renderer/utils')
       }
     },
    

    生产和开发的跨域问题

    dev 是开发时启动的指令 build 是预发布时 webPack 打包的指令

    假设笔者只是一个前端,通常呢,在开发调试过程当中,无法避免需要与后端的同学进行 API 的对接,那也就难免会出现跨域问题。当然传统 javaWeb 不需要跨域,(ip 域 端口 任何一个不同皆为跨域) 在 DEV 模式调试中,我们都是尽量选择前端环境规避跨域问题,而不会去额外搭建 nginx 或更改后端代码。

    跨域只是针对 JavaScript 的,因为开发者认为浏览器上的脚本是不安全的。

    既然我们的 vue 项目是 node 全家桶,依靠 node、webPack 编译 我们直接配置 node 的 proxyTable 作为开发的代理器,这样最简单,依次配置,团队受益。

    cnode 掘金 社区 API 举例

    https://cnodejs.org/api 上边 cnode 的 API 是可以随意调用的,因为后端做了处理。

    看看掘金的: https://xiaoce-timeline-api-ms.juejin.im/v1/getListByLastTime?src=web&pageNum=1 请求一下,不出意外浏览器做了跨域报警。

    哦,我们适配一下 node 代理

    官方例子在这: https://vuejs-templates.github.io/webpack/proxy.html

    扩展一下 proxyTable:

    
    proxyTable: [{
          //拦截所有 v1 开头的 xhr 请求
          context: ['/v1'], 
          target: "https://xiaoce-timeline-api-ms.juejin.im",
          cookieDomainRewrite: {
            // 不用 cookie
          },
          changeOrigin: true,//重点,此处本地就会虚拟一个服务替我们接受或转发请求
          secure: false
        }],
    
    

    再次发送请求。

    • 愉快的拿到了数据

    这样,前后分离的项目可以这样借助 swagger 测试接口,不需要骚扰任何人。实现自己的业务逻辑,简单实现一点。

    代码:

    // blog/index.vue
    <template>
      <div class="Blog">
        <h1>{{ msg }}</h1>
         
      <div v-for="(blog,index) in blogList" v-bind:key="index">
        <h3 >
          <a :href="`https://juejin.im/book/`+blog.id" >
            <span>{{blog.title}}</span>
          </a>
          
          </h3>
      </div>
         
      </div>
    </template>
    
    <script>
    export default {
      name: "Blog",
      data() {
        return {
          msg: "掘金小册一览",
          blogList: []
        };
      },
      created() {
        this.getBlog();
      },
      methods: {
        getBlog() {
          this.$http.get("/v1/getListByLastTime?src=web&pageNum=1").then(res => {
            this.blogList = res.data.d;
          });
        }
      }
    };
    </script>
    

    发布

    • 扫盲

    写完代码之后部署到线上,是不会在线上 clone 代码之后 Npm run dev 的😆,那样会有太多太多的垃圾依赖,为用户带来了灾难性的网络请求,通常借助 webPack 打包之后发布到服务器的 web 服务当中。

    运行

     npm run build
    

    打包目录是之前配置的 webpack

    好了,很多人直接双击 index.html 是不行的。

    Tip: built files are meant to be served over an HTTP server. Opening index.html over file:// won't work.

    需要 http 服务启动,可以扔到本地或服务器的 nginx、apache、tomcat 等容器测试,我通常使用 python 启动一个 http 服务来运行(脚本地址)、当然,自己 ide 支持 http 启动也可以。

    生产中的跨域

    生产当中,因为前端后端必然不同端口,避免跨域,通常使用 nginx 的正向 /反向代理作为跨域的手段。(并非负载均衡,两个概念)

    server {
            listen       80;
            server_name  localhost;
     
            location ^~ /v1 {
            proxy_pass              https://xiaoce-timeline-api-ms.juejin.im;#代理。
            proxy_set_header        X-Real-IP $remote_addr;#转发客户端真实 IP
            proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header        Host $http_host;
            }
     }       
    

    简单配置一下就可以了,不多讲,前端同学了解一下就可以了,nginx 能干的事情还有很多。

    API 的规范化

    你是否为了找某一个业务的接口头痛 你是否还在使用全局搜索找自己的接口 你是否某一个接口不同组件重复写了多次 整理一下自己的接口吧,像上文的 router 一样整齐的划分吧。

    /renderer 下建立一个 api 的文件夹

    webpack.base.conf.js 添加一条 api 的别名,方便我们日后大量调用

    'api': resolve('src/renderer/api')
    

    我们建立 /renderer/api/juejin.js

    
    import axios from 'axios'
    
    let Api = Function()
    
    Api.prototype = {
        getBlog(page, fn) {
            axios.get(`/v1/getListByLastTime?src=web&pageNum=${page}`).then(res => {
                // if (res.data.code === 200) {
                fn(res.data)
                // }
            }).error()
        }
    }
    export default new Api()
    
    
    

    修改一下我们刚才 /blog/index.vue 的 Axios 请求:

    • 先引入 api
    import juejin from "@/api/juejin";
    

    注掉之前离散的 axios,使用从 api 中定义过的 XHR 请求数据。

    getBlog() {
          // this.$http.get("/v1/getListByLastTime?src=web&pageNum=1").then(res => {
          //   this.blogList = res.data.d;
          // });
          juejin.getBlog("1", response => {
            this.blogList = response.d;
          });
        }
    

    OK,很多同学不理解,本来简单的事情为什么搞复杂了?其实这样不复杂,看起来还很清晰,倘若一个大项目上千个请求,不仅重复导致代码覆盖率低,看起来还很乱,不利于团队协作。本系列文章在带领大家前后分离的基础上,会为大家提供更多有利于团队的开发方式,养成良好习惯。

    本章全部代码在这里

    关于我

    往期文章

    《从零构建前后分离 WEB 项目》 序 - 开源的意义

    《从零构建前后分离 web 项目》:开篇 - 纵观 WEB 历史演变

    《从零构建前后分离 web 项目》探究 - 深入聊聊前后分离架构

    《从零构建前后分离 web 项目》准备 - 前端了解过关了吗?前端基础架构和技术介绍

    《从零构建前后分离 web 项目》实战 - 5 分钟快速构建规范的前端项目骨架

    《从零构建前后分离 web 项目》实战 - 欲善其事先利其器 继续打磨前端架构

    1 条回复    2018-10-24 09:10:11 +08:00
    wensonsmith
        1
    wensonsmith  
       2018-10-24 09:10:11 +08:00
    已 block
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2923 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 22ms · UTC 14:58 · PVG 22:58 · LAX 06:58 · JFK 09:58
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.