如何构建一个大型Vue应用程序

1天前 (02-28 01:57)阅读2回复0
kewenda
kewenda
  • 管理员
  • 注册排名1
  • 经验值518335
  • 级别管理员
  • 主题103667
  • 回复0
楼主

如何构建一个大型Vue应用程序

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

搭建什么?

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

快速原型开发

纯静态页面项目(如组件文档、产品首页)

业务前端项目(支撑业务变化,有前后端交互,要经过测试环境验证才能上线)

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

用什么搭建?

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

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

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

此外,脚手架还可以帮你创建目录结构,拉取依赖资源,生成 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 可以直接进入项目创建流程。

步骤中有些地方值得一提

详情:包管理器指定 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 文件,保存时自动格式化代码,保证代码书写效率。

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

模块规划

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

清晰的目录,统一的资源引用方式。

根据路由懒加载模块,提高首次访问速度。

开发环境下只编译自己关心的业务模块,保证 Hot reload 效率。

打包分析,便于定位大模块,作出优化决策。

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

对于路由规划,细心的你可能在初始项目的 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),但这还不够。在业务项目场景下,项目承载多个业务模块,向下治理几十甚至几百个页面,是十分常见的。

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

上图中,路由部分真实对应着代码里的 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

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

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

而打包后,我们可以借助 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)访问到打包后的可视化结果。

从上图 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 已不在打包资源中。

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 应用。希望它能帮助你在前端生态的繁杂今天,迅速搭建起属于你的应用。

0
回帖

如何构建一个大型Vue应用程序 期待您的回复!

取消
载入表情清单……
载入颜色清单……
插入网络图片

取消确定

图片上传中
编辑器信息
提示信息