2019 年如何搭建你的 Vue 项目

Johnston(e)Miranda 发布于3月前
0 条问题

在刚过去的 VueConf 2019 上,Vue 公布了 3.0 的最新进展,其背后有着颇为活跃的社区支持,和完善的配套工具。那么在如今丰富而复杂的 Vue 生态下,我们又如何搭建一个前端项目呢?

搭建什么?

你可能会问:「搭建什么?刚刚不是已经说过了?当然是搭建 Vue 前端项目啊!」但如果再往下想一步,前端项目也有很多种,面向的场景也不一样。比如:

  • 快速原型开发
  • 纯静态页面项目(如组件文档、产品首页)
  • 业务前端项目 (支撑业务变化,有前后端交互,要经过测试环境验证才能上线)

而今天我们要讨论的是第三种。业务项目是面向生产的,它对搭建策略有着较高的要求,往往需要兼顾开发体验、维护成本、生产稳定性、扩展空间等不同维度。

用什么搭建?

前端项目最基础的职责,就是管理前端静态资源,这些资源包含模版文件、样式文件、脚本文件和其他媒体文件。因此资源打包工具是项目中十分重要的一员。

目前,业界占有统治地位的前端打包工具,无疑是 Webpack。然而随着成百上千 Webpack 插件的争鸣,如今想合理地配置 Webpack ,从 0 开始往往要琢磨很久。

有没有一个工具能帮我整合业界最佳配置呢?答案是有,它就是vue-cli 脚手架。vue-cli 基于 webpack 工具链,不但整合了默认配置,还提供了简洁的接口,让你在必要时调整配置,贴合自己的业务。

2019 年如何搭建你的 Vue 项目

此外,脚手架还可以帮你创建目录结构,拉取依赖资源,生成 demo 页面等。而完成一系列准备工作,只需以下简单三步:

#安装vue-cli
npm install -g @vue/cli

#创建项目
mkdir projects && cd projects
vue ui # 通过图形界面创建项目
vue create create-vue-app-in-2019 # or 通过命令行创建项目

#选择项目偏好,生成项目

这就像是为大厨准备好厨房、厨具、调味料,大厨只要关心怎么给客人做菜。

那么,现在怎么做好菜呢?

怎么搭建?

创建项目

为了方便讲解,我们采用图形界面创建项目。通过 vue ui 命令启动 vue cli service 后,访问 http://localhost:8000/project/create 可以直接进入项目创建流程。

2019 年如何搭建你的 Vue 项目

步骤中有些地方值得一提

详情 :包管理器指定 npm,后续在npm 配置文件(.npmrc)中可手动指定国内镜像源,无需安装其他包管理器。

预设 :「默认」预设中的功能比较单薄,我们选择「手动」预设,勾上一些适用于业务项目的功能。

功能

  • 默认勾上的「Babel」负责 JS 和 Vue 模版语法解析,建议打开。
  • 「Router」 负责前端路由功能,业务项目必备。
  • 「CSS Pre-processors」负责样式文件的预编译,使用 sass/less/stylus 写样式必备。
  • 「Linter / Formatter」负责代码规范,业务项目涉及多人长期维护,必备。
  • 「使用配置文件」负责将不同功能的配置拆分到根目录下,便于维护,建议打开。

配置 :Lint 格式化过后的代码与自己写的原始代码往往有不同,这里建议在保存时就观察变化,勾上「lint on save」在命令行中报警;而对于「 Lint and fix on commit」个人认为过于自动,提交 commit 还是以自己审查过的版本为好,谨慎选择。 小技巧:使用 vscode 插件「ESLint」,它会读取根目录下的 .eslintrc.js 文件,保存时自动格式化代码,保证代码书写效率。

创建步骤完成后,我们就得到了一个完整的项目( 查看源码 )。

2019 年如何搭建你的 Vue 项目

模块规划

目前我们创建的项目完全可用于开发原型项目,但离一个的业务项目还有距离。在业务项目中,随着时间的推移,项目会加入一个又一个的页面、模块,所以我们至少可以从几个方面规划这些模块。

  • 清晰的目录,统一的资源引用方式。
  • 根据路由懒加载模块,提高首次访问速度。
  • 开发环境下只编译自己关心的业务模块,保证 Hot reload 效率。
  • 打包分析,便于定位大模块,作出优化决策。

对于目录结构 ,我们在初始项目的基础上新增一些 业务内容

2019 年如何搭建你的 Vue 项目

对于路由规划 ,细心的你可能在初始项目的 src/router.js 中发现了这样一段代码:

// ...
{
      path: "/about",
      name: "about",
      // route level code-splitting
      // this generates a separate chunk (about.[hash].js) for this route
      // which is lazy-loaded when the route is visited.
      component: () =>
        import(/* webpackChunkName: "about" */ "./views/About.vue")
    }
// ...

显然 vue cli 已经在有意引导你做路由级别的代码分片(code-splitting),但这还不够。在业务项目场景下,项目承载多个业务模块,向下治理几十甚至几百个页面,是十分常见的。

2019 年如何搭建你的 Vue 项目

所以在路由分片的基础上,我们设计了一个业务模块层,以便分块开发。

2019 年如何搭建你的 Vue 项目

上图中,路由部分真实对应着代码里的 router 对象与 vue 组件。它们是这样的:

{
    path: "/business-a/page-a",
    name: "business-a-page-a",
    component: () =>
      import(/* webpackChunkName: "business-a-page-a" */ "path/to/business-a-page-a/index.vue")
}

而业务部分与页面部分是我们根据业务需求虚拟出的层级,那么它们在代码里怎么体现呢?答案就在 src/router.js 与 各业务模块(如「business-a」 )的 index.js 中。 在看具体代码前,我们回顾一下初始项目的 src/router.js ,显然,router 入口文件里 直接治理 所有页面的路由。 而基于前面的路由治理方案,我们 将具体页面路由下放给各业务模块分治,router 入口只负责治理业务模块

// src/router.js
import Vue from 'vue';
import Router from "vue-router";

Vue.use(Router);

export default new Router({
  routes: process.env.VUE_APP_MODULE
    ? [
            // 根据环境变量编译单个业务模块
            ...require(`./views/${process.env.VUE_APP_MODULE}`).default, 
        ]
    : [
            // 环境变量没有指定,则编译全部业务模块
            ...require('./views/business-a').default,  // A 业务路由入口
            ...require('./views/business-b').default,  // B 业务路由入口
            ...require('./views/business-n').default,  // n 业务路由入口
        ],
});

业务模块的 index.js 负责连接组件和导出路由配置:

// src/views/business-a/index.js 文件
export default [
  {
        path: '/business-a/page-a',
        name: "business-a-page-a",
        component: () => 
            import(/* webpackChunkName: "business-a-page-a" */ './business-a-page-a/index.vue'),
  },
  {
        path: '/business-a/page-b',
        name: "business-a-page-b",
        component: () => 
            import(/* webpackChunkName: "business-a-page-b" */ './business-a-page-b/index.vue'),
  },
  //...
];

对于部分编译模块 ,上面 src/router.js 中有提到,通过设置一个 process.env.VUE_APP_MODULE 环境变量,可以指定模块名来构建一个模块,不用全部构建,从而提升开发效率。

你可以在根目录下创建一个 .env.development.local 文件在开发时指定一个模块作为环境变量:

#.env.development.local
VUE_APP_MODULE=business-a

当然也可以通过撰写 一个启动脚本 来做到更好的启动体验。

2019 年如何搭建你的 Vue 项目

对于打包分析 ,在编码过程中,借助「Import Cost」 vscode 插件,我们可以实时感知引入模块的大小。

2019 年如何搭建你的 Vue 项目

而打包后,我们可以借助 webpack 插件 「 webpack-bundle-analyzer 」可视化地分析打包结果。

其配置:

// vue.config.js
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = {
  configureWebpack: {
    plugins: [
      new BundleAnalyzerPlugin()
      // 其他 plugins ...
    ]
  }
};

npm run serve 启动主项目后,我们可以从 Analyzer 监听的端口(默认是 8888)访问到打包后的可视化结果。

2019 年如何搭建你的 Vue 项目

从上图 Analyzer 输出的结果中,我们可以看到类似 Vue(vue.rumtime.esm.js)、VueRouter(vue-router.esm.js)这样的三方库占用了大面积打包资源。这样的三方库随着项目迭代,可能会越来越多(如 elementUI、moment 等),而它们又不跟随业务的变化而改动代码,不需要打包。我们可以借助 webpack 的 externals 选项将他们抽离出来:

// vue.config.js
module.exports = {
  configureWebpack: {
        // ...
        externals: {
          vue: "window.Vue",
          "vue-router": "window.VueRouter"
          // 其他三方库 ...
        }
        // ...
  }
};

当然别忘了以 CDN 的方式将它们挂到 window 上

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
   <!-- ... -->
  <body>
    <!-- ... -->
    <div id="app"></div>
    <script src="//shadow.elemecdn.com/npm/vue@2.5.16/dist/vue.runtime<%= process.env.NODE_ENV === 'production' ? '.min.js' : '.js' %>" crossorigin="anonymous"></script>
    <script src="//shadow.elemecdn.com/npm/vue-router@3.0.1/dist/vue-router<%= process.env.NODE_ENV === 'production' ? '.min.js' : '.js' %>" crossorigin="anonymous"></script>
    <!-- built files will be auto injected -->
  </body>
</html>

重新 npm run serve 后,再次查看 Analyzer,Vue 和 VueRouter 已不在打包资源中。

2019 年如何搭建你的 Vue 项目

Webpack 打包优化是一个有趣且富有细节的话题,不乏详细介绍的文章,限于篇幅,今天我们只介绍分析工具和典型的优化手段。

好了,经过模块规划后,目前的项目相比初始项目有了一些变化,具体可以 查看这里

前后端交互

在前端 HTTP 客户端工具完善,后端普遍服务化的今天,Web 应用的前后端交互模式比较典型的是,前端静态资源和服务端分别部署在不同域下,前端跨域调用 API 服务,组装数据,渲染页面。而我们希望开发业务项目时:

  • 调用方式一致,请求代码流程清晰
  • 匹配开发、测试、生产不同环境
  • 跨域支持
  • 请求错误处理一致

HTTP 客户端中, axios(支持 IE11+) 是最受欢迎的 JS 客户端,如果要兼容低版本浏览器,则可以考虑 vue-resource(支持 IE9+) 。它们都支持 promise 调用、拦截器等较现代的请求功能,这里我们引入 axios 作为 HTTP 客户端。

支持 promise 调用的好处之一是,在 babel 插件的支持下,我们可以无缝使用 async/await 语法(经 babel 转化后就是 promise)。我们将业务 API 统一收口到前面提到的 src/api 目录下,导出供业务页面使用。

// src/api/douban-movies.js 

import axios from "axios";
import { douban } from "@/config/hosts";

// 导出 API 资源调用函数
export const getTop250Movies = params =>
  axios.get(`${douban}/v2/movie/top250`, { params });
// src/views/douban-movies/top250/index.vue 

// script 部分
import { getTop250Movies } from "@/api/douban-movies";

export default {
  name: "Top250",
  data() {
    return {
      movies: []
    };
  },
  async created() {

    // 调用 API 资源
    const { subjects } = await getTop250Movies({
      start: 0,
      count: 20
    });

    this.movies = subjects;
  }
};

以上, src/api/${business}.js 只负责导出不同 API 的调用函数,而不去关心在不同业务下是怎么调用,传了什么参数。而对于业务页面,不同页面引入 API 资源的方式一致,API 资源可以多处复用。此外 async/await 语法可保证以同步代码的形式书写异步代码逻辑,流程上比 promise 和 callback 都要直观。

处理前端资源部署在 不同环境 (往往是通过域名区分)时,我们把管理域名的任务交给 src/config/hosts 文件。例如:

const getEnv = () => {
  /**
   * 这里写判断环境代码,
   * 最终返回对应的环境标识
   */
  return "prod";
};

const env = getEnv();

// 不同环境标识输出不同 host
export const douban = {
  prod: "//douban.uieee.com",
  pre: "//douban-api.now.sh",
  test: "//douban-api.uieee.com"
}[env];

这里我们假设豆瓣 api 分别有三个环境,test、pre 和 prod,分别对应三个环境能调通的域名(这里只是假设,实际它们都是线上域名)。getEnv 会按照不同需求判断环境标识,比如当前页面部署在 https:// test.douban.com/ 下,getEnv 匹配到 "test" 并返回,则导出的 douban 变量的值是 //douban-api.uieee.com 。

如果你的前端资源不是部署到 API 的域名下,那么显然是 跨域调用 API 。除了要联系服务端配置CORS ,允许前端资源所在域名的请求外,还需要 给 HTTP 客户端设置 withCredentials ,确保跨域请求时带上 Cookie 等身份信息用来给服务端鉴权。

API 统一错误处理 并不是靠前端单独完成的,而是需要两端协商,遵守标准 HTTP Code(或约定好的其他标准),再基于约定,通过 axios 的拦截器统一处理错误,例如:

// src/utils/net.js

//这里使用 element-ui 的消息UI组件
import { Notification } from "element-ui";
import axios from "axios";

/**
 * 注册全局 Axios 拦截器
 */
export function registerInterceptor(options) {
  // ...
  axios.interceptors.response.use(
    res => {
      const ret = res.data;
      return Promise.resolve(ret);
    },
    err => {
      const status = err.response && err.response.status;
      let message = "";

      switch (status) {
        case 404:
          message = "请求资源未找到";
          break;
        case 401:
          message = "无权限";
          setTimeout(() => {
            //处理登录失效,例如跳转到登陆页
          }, 1000);
          break;
        case 500:
          message = "服务器异常";
          break;
        // 其他错误...
        default:
          break;
      }

      Notification.error({
        title: "错误",
        message: message || err.message
      });

      return Promise.reject(err);
    }
  );
}

在入口文件中注册拦截器。

// src/main.js

// ...
import { registerInterceptor } from '@/utils/net';

//...
registerInterceptor();

new Vue({
  router,
  render: h => h(App),
}).$mount('#app');

前后端交互部分可以 点击这里 参看源码。

输出生产资源

在开发环境验证应用没有问题后,我们就可以执行 npm run build 命令构建生产版本了。构建完成后,Analyzer 依然会自动打开浏览器,显示打包结果,与此同时,你可以在 dist 目录下得到资源 打包结果

关于部署,默认情况下,在打包过程中通过插件自动注入 dist/index.html 文件内的资源,会以 /js/app.22d24e62.js 这样的相对路径作为资源路径。如果你的网站访问入口和前端资源部署在 同一域名 下,没有什么问题。

然而,为了提升静态资源访问速度,降低主域名下服务器的流量压力,我们通常会将静态资源从主域名下剥离出来,托管在 CDN 上。例如访问 https:// h5.ele.me ,其页面内的静态资源就托管在 https:// shadow.elemecdn.com 下。这种情况下,相对路径无法工作。我们要在配置中指定 publicPath 以指定静态资源的域名。

// vue.config.js

module.exports = {
  //..
  publicPath: process.env.NODE_ENV === "production" ? "//some.cdn.com" : "/",
};

我们看一下生产构建结果,相对路径前已经添加了 CDN 域名。

<!-- dist/html 文件内注入的脚本 -->
<script src=//some.cdn.com/js/chunk-vendors.f4718524.js></script>
<script src=//some.cdn.com/js/app.84426696.js></script>

小结

围绕怎么搭建 Vue 前端项目这个问题,今天我们从搭建什么项目,用什么搭建项目,怎么搭建项目三个角度梳理了一个通用 Web 应用的搭建过程。初步搭建了一个支撑业务变化,有前后端交互,适配不同环境的 Vue 应用。希望它能帮助你在前端生态的繁杂今天,迅速搭建起属于你的应用。

参考

快速原型开发

Vue CLI

Webpack 应用瘦身

Webpack 插件:webpack-bundle-analyzer

Axios

CORS

查看原文: 2019 年如何搭建你的 Vue 项目

  • whitetiger
  • heavyrabbit
  • tinycat
  • purpleelephant
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。