三万字,100张图,1个小时,带你整体了解 vue 前端架构
整体大纲的把握这个系列文章我尽量照顾前端的同学和后端的同学,不穿插讲述。尽量按照前端-后端-部署-运维来讲,当然中途涉及到跨域、Rest、oAuth2.0这种前后协调的还是无法避免捎带一笔。比如:这种目录阅读可能对于只掌握前端/后端一种技术栈的同学,或只想去看后端/前端/运维的同学看上去非常的难受、我会修改大纲避免这个问题,循序渐进的来。你会学到深入了解前后分离,了解常见架构前端后端项目的搭建与优
整体大纲的把握
这个系列文章我尽量照顾前端的同学和后端的同学,不穿插讲述。尽量按照前端-后端-部署-运维来讲,当然中途涉及到跨域、Rest、oAuth2.0这种前后协调的还是无法避免捎带一笔。
比如:
这种目录阅读可能对于只掌握 前端/后端 一种技术栈的同学,或只想去看后端/前端/运维 的同学看上去非常的难受、我会修改大纲避免这个问题,循序渐进的来。
你会学到
- 深入了解前后分离,了解常见架构
- 前端后端项目的搭建与优化
- 前端后端技术的选型
- 开发过程中提升效率的小技巧
- 不同场景下跨域的N种解决办法
- 几个良好习惯提升 debug 的能力
- 前端的 Hybrid 开发自己的 APP
- 前端 SPA 模式的优化
- 学会爬取我们想要的资源
- 后端架构如何向微服务转型
- 如何使用 docker 部署微服务
- 如何维护项目的运转
如何从零打造一个前后分离的互联网主流 WEB 项目、真心希望我能帮到你们。
Part 2 : 纵观WEB历史演变
在校学习和几年工作工作中不知不觉经历了一半的 WEB 历史演变、对近几年的发展比较了解,结合经验聊聊 WEB 发展历史。
演变不易,但也是必然,因为为人始终要进步。
WEB 的发展史
一、开山鼻祖 - 石器时代
静态网站
这是 1997 年 Apple 官网,那时的网站不如叫网页,像一张浮夸的彩色报纸,那时是纯粹的 HTML 时代,不管你是不是访问这个网页,每个页面都是在服务器上存在的。
CGI技术
随后技术性强一点的网站可能会通过 CGI Perl 运行一小段代码与数据库或文件系统进行交互。比如:
这是1998 年的 Google ,为了达到搜索条件,不可能用大量的人力去堆砌静态页面,所以使用这种方式“曲线救国”,但是 CGI 伸缩性不是太好:每个请求分配一个新的进程,不太安全(直接使用文件系统或者环境变量),同时也没提供一种结构化的方式去构造动态应用程序。
静态网站是最受搜索引擎欢迎的网站,因为它相对固定,所以网站 SEO 非常好做,我猜测这也是为什么现在的文档网站大部分都是静态网页的原因之一吧。
很可惜我没能亲眼看一看这样的时代
二、前人种树 - 文明时代
asp 和 jsp
2005 年左右,先后出现了 微软的 ASP 和 Java Server Pages [JSP] 等技术,取代了 CGI ,增强了 WEB 与服务端的交互的安全性、用起来也更加简单,但随着各个公司WEB业务的复杂性,缺点也逐渐暴露出来:
1、技术单一,难以维护
JSP页面由HTML代码和嵌入其中的Java代码所组成,用一个比较常见的 JSP 代码段举例:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<%@ page import="com.zifangsky.OnlineFriend.model.article.ShowByPage"%>
<jsp:useBean id="showAllTitle" type="com.zifangsky.OnlineFriend.model.article.ShowByPage" scope="session"/>
JSP = HTML+Java
上面的代码 HTML 中大量耦合了JAVA代码,通过JSP编译之后可以在客户端充当部分服务端的角色,这让我们难以搞清服务端的角色,以及增加调试的复杂度。业务稍微复杂一点,试想一下:HTML中掺杂了太多java代码,不论是开发还是维护都是一件痛苦的事情。
2、不不够灵活
JSP与Java Servlet一样,是在服务器端执行的,通常返回该客户端的就是一个HTML文本。我们每次的请求:获取的数据、内容的加载,都是服务器为我们返回染完成之后的 DOM,这也就使得我们开发网站的灵活度大打折扣,在这种情况下,同年:Ajax火了。
AJAX 的出现
为什么说 2005 年 Ajax 火了?因为 Ajax 技术并不是 2005 年出现的,他的雏形是 1999 年。
1999年,微软公司发布IE5,第一次引入新功能:允许javascript脚本向服务器发起HTTP请求[这也就是今天万恶的 ActiveX 原型]。这个功能当时并没有引起注意,直到2004年Gmail发布和2005年Google Map发布,才引起广泛重视
Google做了什么事儿?
在 2005 年,Google 通过其 Google Suggest 使 AJAX 变得流行起来,他大概是这样的事情:
现在看来很常见的技术手段,当时迅速燃爆了技术圈,以此来实现:异步交互
这样既能增加用户的体验,又能替代掉页面部分的服务端代码,从此, AJAX 成为脚本发起 HTTP 通信的代名词,次年 W3C 也在 2006 年发布了 AJAX 的国际标准
总结:
随后各种 JSP ASP 的改良模板引擎、全新的交互方式也如雨后春笋一般涌现。并且以 JAVA 作为服务端也出现了如 Struts 、 Spring、Hibernate 的老一代框架、采用后端 MVC 的方式让构建 WEB 应用再一次更加健全, WEB 服务正在逐渐由石器时代走向文明时代。
三、化繁为简 - 工业革命时代
时光啊不断地飞逝,前端后端也出现了几个潮流。
前端发展
移动端
手机已经发展出了一些苗头,网页也区分了web和移动应用两种模式,但移动端限制于当时手机行业的技术,发展较慢。
Jquery的出现
出现了非常流行的JavaScript库:jquery,能够快速构建动态、美妙的web应用,完美的封装了Ajax,让开发者开发网页变得优雅。
SPA的雏形
随着文明时代 Ajax 正式提出,加上 CDN 开始大量用于静态资源存储,于是出现了 SPA (Single Page Application 单页面应用),Backbone EmberJS AngularJS 这样一批前端框架随之出现,但以当时的配套技术来说,SPA 道路并不好走:例如 SEO 问题、SPA 过多的页面、复杂场景下 VIEW 的绑定等,都没有很好的处理。
后端发展
Struts 、 Spring、Hibernate 经过几年的发展、SSM这个今天被我们说烂了的词、当时几乎成了当时 JAVA 服务端的 首要选型,我想这也是为什么很多公司、或外包公司依然维护这样一套架构的主要原因。
总结
这几年的飞速发展,为我们节约了大量的经历、降低了开发者和开发过程的门槛,极大提升了开发效率和迭代速度,我称之为工业时代
经历
说出来你可能不信:大三快结束时实习求得的第一份工作,一个人断断续续开发7、8个月,就是钻研这些自技术栈,独立开发出一款web应用 微宝创业,惭愧的说:
项目架构从文明时代 -> 走到最后的工业时代!不断的重构,不断的上线 ,拼命的学习,我很感谢当时老板对我的信任和同事对我的帮助。
四、百家争鸣 - 技术大爆炸时代
时光啊他一刻不停,直到今天 -- 技术只能用爆炸来形容。
前端爆炸
工业时代提出的 SPA 模型随着 NODE 的兴起、服务端、各种工具、容器的飞速发展、前端 MVC MVVM 模式逐渐清晰、前端涌现了相当一批优秀的开源项目:
包管理: npm yarn
打包:grunt gulp
模块加载:RequireJS SeaJs
框架:VUE Angular React
hybrid :ionic weex react-native electron
预处理器:less sass
数据可视化:echarts hcharts
以及提升用户体验的动画,让我们更有“面子”
甚至前端也可以使用 Node 来构建自己简单的服务端、正在逐渐摆脱“客户端开发者”的角色
后端爆炸
go
更适合面向服务器编程,以前你如果使用C或者C++做的那些事情,用Go来做很合适,例如、虚拟机处理、文件系统等,强如 docker Kubernetes(k8s)都是 GO 写的
python
像一门生物语言,目前看来更容易处理算法、人工智能、网络爬虫、运维方向
java
一款20多年的语言,不断的变强。涌现了很多高质量的库,几个有代表性的:
netty rebbitmq:轻松实现消息队列
elasticSearch: 轻松实现搜索引擎
spring-boot: 面向配置,更加轻松的构建web服务端
spring-cloud、dubbo: 轻松构建微服务
以及即将迎来的 强悍的JAVA11
还有 持续集成 云服务 devops 等运维相关
总结
go 和 python 的出现让我们服务端开发者能做更多的事情,比如自动化运维、写中间件。逐渐偏向全栈方向发展。而 JAVA 20多年来的生态圈子发展,能帮助我们写出更健壮的服务。以及狠狠向我们砸来的:人工智能、devops、云服务等技术,令我们眼花缭乱,开源成为了一种潮流,技术分享成了每个人都想做的事情,我称之为:技术爆炸的时代
经历
我近两年很烦恼:如何才能让前后端更加优雅的通信?
曾经使用多种后端模板引擎直到完全摒弃,后到 node 做代理、渲染 + grunt 进行数处理,之后逐渐使用
vue + webpack ------> Rest API
这样如果不得不用 NODE 也只会成为 Rest 中的一员而不用经过 NODE 做繁琐的通信了。这种前后分离的方式达到了满意的效果,前端不必再管后端的事情,后端?写好自己的服务就好了。
Part 3 聊聊前后分离架构
前后分离,一直是一个相当泛泛的问题,前后分离到底好不好?没有绝对的对,没有绝对的错,业界就这个问题已经激烈的探讨几年了.出现讨论的点在于:分离当然是好的,但是以什么样的服务需要进行前后拆分?拆分到什么粒度?前后端如何配合?
截图时间: 2018-08-30 - Github
我们随意在 Github 输入前后分离关键字,看下搜索的结果: 1K 的库 11k 的 Issues 足以说明前后分离的趋势,可以想象激烈程度,业界比较有名的讨论:Web 前后端分离的意义大吗?,值得一提的是:前排对于这个问题讨论比较深刻的大部分都是全栈工程师。因为全栈对全局的了解相对比单纯做前端、后端全局观念更强一些,考虑的问题更多一些。
筛简历引发的思考和分析
后端职位的怪圈
在公司的简历库随手截几个局部的图,近两年面试过很多的 1-3 年 java 开发者,在筛选简历和面试过程中,也发现了几个问题:相当多一部分 javaer 技术栈上总是多了那么个 HTML ajax jquery bootstrap easyUi ,看起来很唐突,如果面试提到了前端技术栈,基本没有能答的很好的,甚至有的人连 原型链 都不知道。这也是大部分人对全栈的误解,其实我是不太感冒这样的简历的,因为没有什么亮点,技术栈不是写的越多越好,总结起来:他们对前端的掌握很基础,勉强能胜任一些业务上的工作。那为什么这么多人都掌握一些前端技术呢?我分析可能有三点:
思考原因:
- 培训机构的兴起,机械化的教学
- 求职者自身的兴趣
- 一些公司的技术栈不全,对技术没有追求,大部分用的几年前的架构,前后业务耦合很大,市场缺口大
分析
因为我也维护过几个月的敏感项目,深有体会,只写服务端的人是无法胜任这项工作的,
如果多数的 开发者 这样的简历,可以推测:现在的 IT 行业中前后端糅杂一起的架构还是存在、并且有一个量级,这导致他们不得不寻找一些懂得一点前端技术的人来开发项目,减少沟通的成本,加快项目的进度,这也就催生了很多所谓的 web 开发培训机构。
你问我当年维护的开心吗?一会告诉你。
什么是前后分离
前后端分离并不是什么新鲜事,到处都是前后端分离的实践。然而一些历史项目在从一体化 Web 设计转向前后端分离的架构时,不可避免的会遇到各种各样的问题。由于层出不穷的问题,甚至会有团队质疑,一体化好好的,为什么要搞前后端分离?说到底,还是技术和思维方式没转变过来。
一体化模式其实在上一开篇:纵观历史演变 中已经提到过了,不在赘述。
前后分离看起来应该是这样的:
前后分离就是在架构层次上 构建项目或对现有的项目 客户端 服务端 分离开,减少前后端代码的耦合度,大家一致认同的前后端分离的例子就是SPA(Single-page application) ,所有用到的展现数据都是后端通过 JSON 但不仅限于 JSON 的方式提供的,前端只管展现,提供更好更绚的交互,后端只管提供更健壮的高可用服务。
千万不要有先写项目,写完再重构的想法,项目初期能一步到位最好,何必再去重构,然后不得已抛弃一些已经写完的组件、库浪费人力呢?
前后分离解决了什么问题
每个人各尽其职
好的开发者是可与不可求的,若寻找一个 优秀的 full_stack 更是难,从校招进行培养也不太实际,招一个能力一般的程序员,技术驱动性比较差,甚至拖慢产品迭代。分离开来我们就可以专注于 前端、 服务端 领域去寻找专业的人才。
解耦
前端后端代码大量耦合代码看起来是这样的:
看看简单例子吧:
//(node端处理)
if (is_weixin()) {
init([
'api',
'image',
'xxx',
'...',
], function () {
<%- doSomeGloble %>
});
} else {
}
//接收node端一些数据
let blogs = <%- blogs %>;
let users = <%- users %>;
这还不是 JAVA 模板 而是相对轻量、优雅的 NODE 的 EJS 渲染的,这还好,我见过更令人难受的代码,经常为了一个问题要回头看 N 多代码,这里就不写了。
那么前后分离如何让它们解耦变得更清晰?后面会结合服务端统一补充。
什么项目不适合前后分离
blog、文档
你说你搭建一个博客、 API 文档系统 这种小项目,一个人就可以开发。搞了一个前后分离,需要分离部署。又增加了 SEO 的复杂度,增加了开发的周期、增加了用户部署的难度,何必呢?当然,如果只是技术实践的一种学习方式,还是欢迎的。
前后分离带来的问题,如何解决?
沟通成本问题
前端妹子:哥,获取全部博客调哪个接口?哦,昨天不是发你文件了吗
前端妹子:我找不到了😭
哦,等下吃晚饭发你啊
前端妹子:哥,产品提出根据手机壳自动换主题的需求,你有接口吗?... 我看看...应该...没有!可能需要 Python 的老哥支持!你去找他要吧
前端妹子:哥,你根据 TAG 获取博客的接口写完了没?接口是啥?我好渲染数据啊没等等......
这显然是不规范的,我们期望的是前端后端先约定一个接口协议,后端没完成时,前端自己 mock 测试数据,前端找不到接口的时候,直接查看 API ,根据这个接口协议我们前后端统一编程,那么我们如何处理呢?
如何降低沟通成本?
后端数据服务化,走统一的 REST 接口规范输出,降低前后端接口定义的沟通成本。避免“口头说明”的方式。
什么是 RESTful API ?
所以RESTful API就是REST风格的API。 那么在什么场景下使用RESTful API呢?在当今的互联网应用的前端展示媒介很丰富。有手机、有平板电脑还有PC以及其他的展示媒介。那么这些前端接收到的用户请求统一由一个后台来处理并返回给不同的前端肯定是最科学和最经济的方式,RESTful API就是一套协议来规范多种形式的前端和同一个后台的交互方式。
我通常用 swagger + mock 平台生成标准的 RESTful API,同时也支持扩展多个编程语言例如: Go Python
SEO 问题
以 vue + webpack 的 SPA 为例,没有了后端模板返回的 HTML,前端渲染并不被搜索引擎的爬虫接纳。在日后实战 SEO 之前先通俗渲染呗爬虫识别的区别:
seo 本质是一个服务器向另一个服务器发起请求,解析请求内容。但一般来说搜索引擎是不回去执行请求到的 js 的。也就是说,如果一个单页应用,html 在服务器端还没有渲染部分数据数据,在浏览器才渲染出数据,而搜索引擎请求到的 html 是没有渲染数据的。 这样就很不利于内容被搜索引擎搜索到。 所以服务端渲染就是尽量在服务器发送到浏览器前 页面上就是有数据的。
以博客为例简单聊聊:
- 静态服务
<div>我是正文1</div>
<div>我是正文2</div>
<div>我是正文3</div>
爬虫直接抓到 html 解析 - 生成索引
- 传统后端渲染
@RequestMapping("/index")
public String index(HttpServletRequest request,HttpServletResponse response){
return "welcome";
}
这里就比较有意思了,比如我们打开的网址是:
http://host:port/index
实际充当 Controller 的是 服务端,服务端直接返回渲染好的网页给你,爬虫拿到的也是一样,所以 SEO 没啥太大的问题。
- 前后分离 SPA
let blogs = [];
this.axios.get('/index, {})
.then(res => {
blogs = res.data;
})
.catch(err => {
console.error(err);
});
<!--前端模板渲染dom-->
<div v-for="(item, index) in blogs" :key="item">
同样我们输入http://host:port/index
注意:SPA 通常有自己的路由策略,这也就是前端 MVC MVVM 中的 第一个 M
一个典型的 SPA 站点
我们输入网址先到了这个页面,然后再去异步请求服务器,再由前端页面渲染,又是单页面服务如果我们不做任何处理,那么你将被各大搜索引擎抛弃。
如何解决?
只要做 SEO 的产品就要做服务端渲染,如果你对 SEO 需求有,但要求并不高,仅部分页面、可以曲线救国
nodejs 出现之前有两种解决方式,一是做一动一静两套页面,服务器判断请求来自蜘蛛就呈现静态页,否则呈现动态页;二是服务器架设虚拟浏览器软件,请求过来了先让虚拟浏览器跑一遍,再将得到的静态页面返回给客户端。这两种方式在大型项目上都有性能问题。
有了 nodejs 后主流做法是前后端同构方案,即一套代码在浏览器端和 node 端都可以运行,从而可以先在 node 端请求数据渲染模板,然后将渲染结果返回给浏览器最终呈现。感兴趣可以看看
Vue 的SSR方案
Angular 的SSR方案
如何更细致的研究 SEO 以后再说
跨域
由于采用前后端分离部署,自然不在一个端口,不在一个端口必然跨域,不过这对现在的技术手段来说完全不是问题
开发模式
为了更快更好的开发,dev 下一般采用 node 做代理层,解决跨域,几乎无障碍开发,而且可以轻松切换环境。
部署模式
部署一般不依托 node 进行部署,通常我们发布到 HTTP 服务器,与服务端进行通信,通常使用 nginx 进行正向代理。
Part4 前端了解多少?
技术栈的选择
首先我们构建前端架构需要对前端生态圈有一切了解,并且最好带有一定的技术前瞻性,好的技术架构可能日后会方便的扩展,减少重构的次数,即使重构也不需要大动干戈,我通常选型技术栈会参考以下三点:
一、提出自身业务的需求
- SEO 是否非常重要?
- 主要面向:移动端还是 pc 端?
- 是否有开发 app 的规划?
有了这样的问题我们可以带着问题去重点选型一些这写问题技术方案比较成熟的技术栈。
二、自身是否成熟,文档是否友好
这里举一个以前开发过程中实际遇到的,当时为了优化用户体验,节省开发效率 选型了一款 MVVM 轻量框架,可惜当时没有决定权, CTO 选型了 avalon
当时之所以没有选择 backbone ,主要是因为没有成熟的中文文档,考虑到团队的流动性和上手性暂时没做考虑,最终选择了 司徒正美的 avalon 当时来说还是比较前卫的,也有一些以去哪网为首的大公司都在用。我们当时用的时候 avalon2 刚出不久,直接用的 2.0,使用过程也出现了一点问题:文档离散,这一块那一块,存在后置性,生态少,扩展性价比不高 ,有时候遇到匪夷所思的 bug 寻找原因翻了几遍 demo 、文档 可能会找到答案,没有重点标识。当然就当时来说确实是给我们提升了部分开发效率,但是我可能当时更偏向 Angular 或 vue 的。因为他们有无以伦比的生态圈和各种问题的技术方案以以及完善的开发者文档,值得一提的是 avalon 的作者是兼职维护的,如果全栈运营的话,我相信远比现在更好,看一看 avalon 的源码也会对自己有不少的提升。对于生产的技术选型要更加谨慎。
三、了解其生态系统
上文提到了生态系统,以我比较常用的 vue 来举例,vue 发展至今仅官方为我们提供了以 vuex、 vue-router、 vue-loader、 vue-cli、 vuepress、 vue-devtools、 vue-ssr 为首的 89 个开源项目,包括无数的 vue 相关的 UI 库,vue 插件 甚至是近两年淘宝提供的 Hybrid : weex 的支持
截止今天 github 开源的 与 vue 相关的项目多达 167,752 个,与 angular 相关的多达 416,811 个,与 react 相关的 多达 594,272 个。
统计时间 2018-09-01
我想有了这样的生态支持,完全可以满足我们中小项目的 95% 以上的需求,至于比较哪个更强是没有意义的 。
因为比较熟悉让我斗胆私自选择 vue 作为我们的 SPA 主架构
四、画出我们期望的前端基础架构模型
因为我们上一章选型了 Vue,如果只考虑前端我们最初的想法:技术栈大概是这样的:
通过 node 和 webpack 的支持 把 vue 组件 build 打包成传统元素,发布到 http 服务中,请求后端服务。
随后可能是这样的:
随着目前主流第三方库的越来越多和技术的尝鲜、客户端的需求、或被动[不得不用]、或主动的去引用了 babel less sass *.loader 和 hybrid 等组件库。
再后来的技术栈需要我们根据真正踩坑之后才会逐步完善
可能是 polyfill 懒加载 xss protobuf 等 针对 浏览器兼容、速度优化、 SEO 、通信协议 等具体问题。所以,前期可以不用过多考虑,我们只要知道:这个问题我们是可以解决的,但是现在可以先不去考虑,有些同学,太过于“完美主义”以至于想法不错,但动起手来做了几天就不做了,完美主义害死人。
了解 Webpack
WebPack 可以看做是模块打包机器,它可以分析你的项目结构,找到 JavaScript 模块以及其它的一些浏览器不能直接运行的拓展语言:Stylus、Scss、less、TypeScript、CoffeeScript 等,并将其转换和打包为合适的格式供浏览器使用。比较常用的还可以通过 webpack-dev-server 进行开发模式的热更新
WebPack 是一种模块化开发的方案
当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle
webpack 通过 loader 可以支持各种语言和预处理器编写模块,最后打包为一个(或多个)浏览器可识别的 JavaScript css 文件
目前支持的 loader 列表
了解 ES6
官方说法
ECMAScript 6(简称ES6)是于2015年6月正式发布的JavaScript语言的标准,正式名为ECMAScript 2015(ES2015)。它的目标是使得JavaScript语言可以用来编写复杂的大型应用程序.
科普
很多人总是搞不清楚 ES 这些东西,这里大白话讲讲:
他们的先后顺序是:ES5、ES6(ES2015)、ES7、ES8
在 2015 年 6 月 ES6 的第一个版本发布, 正式名称就是 《ECMAScript 2015 标准》(简称 ES2015)算是 2011 年 ECMAScript 5.1 之后的 6.0版本
2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)[因为改动小,其实他是 6.1 版本,但总有人愿意叫它 ES7 ,不标准的]
2017 年 6 月发布 的《ECMAScript 2017 标准》(简称 ES2017) [因为改动小,其实他是 6.2 版本,但总有人愿意叫它 ES8 ,不标准的]
就像 Kubernetes 人们开他起了一个 K8S 的名字 (K 和 S 中间有 8 个单词),他是不标准的
了解 Babel Traceur
Babel、Traceur 是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:
JavaScript.next-to-JavaScript-of-today compiler
今天就使用未来的 JavaScript
截止发布日期 (2018-09-04) ,没有一款完全支持ES6的JavaScript代理(无论是浏览器环境还是服务器环境),所以热衷于使用语言最新特性的开发者需要将ES6代码转译为ES5代码。
让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
ES7 作者完全没精力看 ,不过 Bable 逐渐替代了 Google 的 Traceur 成为主流了,我是个俗人,所以我选 Bable
了解 Sass Less Stylus
Sass 是不是违反了中国的广告法了??
Sass 、Stylus 和 Less 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句。
- 一张表格对比三语言
语言 | 实现 | 特性 | 赋值 | 缩进 |
---|---|---|---|---|
Sass | Ruby | 变量$开头 | $var: value | 不需要 |
Less | JavaSript | 变量@开头 | @var: value | 不需要 |
Stylus | NodeJs | 不能使用@开头 | var:10 | 都可以 |
你现在可能都已经熟悉了,上文讲 WebPack 讲过: webpack 里使用相关 loaders 进行配置就可以使用了,以下是常用的CSS 处理loaders:
Less Loader
Sass Loader
Stylus Loader
自己去找:loader 列表
像:哪种语言更好、使用的更多、更简单 容易引起争议的 博主不想讨论,看自己喜好
了解 Electron
一个可以使用使用: JavaScript, HTML 和 CSS 构建跨平台的桌面应用的框架,也算 hybrid 的一种,主要场景是 PC 端,没啥好说的。
值得一提的是 Visual Studio Code 、Atom、GIthub Desktop 都是基于此构建的,有时候按 CMD + option + i 有惊喜哦
Part5 快速构建规范的项目骨架 「VUE」
初步搭建脚手架
- Tips
任何不错的开源项目都有 project-cli 脚手架、我们用它生成往往能 快速配制出最佳的、理想的脚手架
我通常使用 cli 生成项目骨架再在之基础上进行个人修改。
什么是 CLI
命令行界面(英语:command-line interface,缩写:CLI)是在图形用户界面得到普及之前使用最为广泛的用户界面,它通常不支持鼠标,用户通过键盘输入指令,计算机接收到指令后,予以执行。也有人称之为字符用户界面
顾名思义 XXX-CLI 就是使用命令行生成的 XXX 程序。之前写过一款 基于 nodeJs 制作个性 CLI 的教程
如何用node开发自己的cli工具并发布到NPM , 想详细了解制作流程的可以简单看看。
vue-cli
截止 2018-09-02 vue-cli 最新版本为 3.0
vue 中文生态非常完善,我们直接去官网看看:
vue-cli2 和 vue-cli3 的对比
很遗憾, vue-cli-3 是 2018-08-11 出来的,而我的论坛早在之前就着手搭建了 cli-3 耽误了我一些时间,后面也会提到
简单看看了看 vue-cli3 的新特性:
- 可以生成 pwa
- 支持 UI 界面勾勾选选就可以了
- 兼容 cnpm 了
- 搞了一套自己的 vue-cli-service 如下:
我这两天不忙的时候就在考虑项目兼容 vli-3 但是后来废了很多时间,效果依然不理想,我回滚了代码宣布放弃了。
鉴于使用 cli-3 并没有对我的项目有性能上的提升,反而翻遍了我的很多成熟的基础架构,为时间成本考虑,我决定还是使用 cli-2 进行开发,大体目录结构都是一样的。
vue-cli 的安装
安装前应注意前提条件,避免浪费不必要的时间。
Vue CLI 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+)。(如果你用的和我一样 也是 cli-2 那么不需要如此新的 nodeJs )你可以使用 nvm 或 nvm-windows 在同一台电脑中管理多个 Node 版本。
不将远离了,官网比我讲的好得多。
可以使用 yarn 或 npm 来安装
npm install -g @vue/cli
# OR
yarn global add @vue/cli
我用 NPM 来重新尝试一次 (对 npm 速度表示不理想的 可以尝试淘宝的 CNPM 不要过度依赖cnpm):
localhost:~ Venda-GM$ sudo cnpm i @vue/cli -g
TIPS
npm 中 install 可以写成 i , -g 放哪都行 ,--save 可以写成 -S , --save-dev 可以写成 -D
看到这个画面,安装完成了。
测试一下查看一下版本是不是正确,ok 创建项目:
vue create new-bee
拉取 2.x 模板 (旧版本)
Vue CLI 3 和旧版使用了相同的 vue 命令,所以 Vue CLI 2 (vue-cli) 被覆盖了。如果你仍然需要使用旧版本的 vue init 功能,你可以全局安装一个桥接工具:
npm install -g @vue/cli-init
vue init
的运行效果将会跟 vue-cli@2.x
相同
vue init webpack my-project #这样来生成一个 *cli-2* 项目
使用 vue-cli-2 生成项目
vue init webpack new-bee
下面是我创建项目我所选的选项:
稍微讲讲下面三个:
- vue build 的方式
推荐使用 运行时 + 编译时,通常需要 webpack 编译 .vue 模板。
- 是否选择预先设定的Eslint
并是不每个人都适合的,有的要求过于严格,我自己有一套成熟的, 代码在这里 , 就用自己的了,当然可以基于它做一些删减。
- 它要帮我们执行 install
如果你有一个好的socket终端代理,可以用这个,否则可以选择 No 自己用 cnpm 执行
初窥目录结构
让我们来看看 vue-cli2 自动生成的项目目录,我打上标签,为可能不太理解的同学简单描述一下
这次我们重构的主要目的是规范、更适合多模块多人协作、而不是为了让它看起来更复杂,本文的项目结构、esLint 改良、等都是经过项目小组反复的推敲决定的,有一定的生产价值。
杞人忧天 : 为 electron 做好准备
cli 生成的项目 src 下面直接就是源码,但是为了考虑以后使用 electron 我们再用 renderer 包裹一下,规范一点。
可以参考一下 electron-vue
兼容Electron的源码目录
tips: 上述截图 github 树形目录的插件是 octotree 也可以在谷歌商店直接搜索安装,看源码省去不少时间。
- 先不建立 electron 的 main 文件夹 和 index.ejs 需要添加依赖,目前暂时用不到。
别忘了改一下 webpack 相关的路径问题
加上 renderer 的路径
app: './renderer/src/main.js'
@ 的路径也要在 webpack 进行修改,否则会不找组件
需要改下 webpack alias [别名] 配置
改之后的样子
alias: {
'@': resolve('renderer/src'),
}
容器级的目录
在 组件目录(components) 同级建立 容器(container) :容器里面的各个模块分离开,这样可以使项目模块看起来更加清晰。如果十多人协作的项目又能很好地对工作区划分,合理的建立路由,避免不必要的冲突。
以目前的 论坛项目为例
路由目录的调整规范
在 /router 下为 /container/blog 建立 blog.js
const Blog = () => import ( /* webpackChunkName: "blog" */ '@/container/blog/index')
/*
所有container/blog目录下的路径都配置在此路由children下,避免混乱
*/
let routes = [{
path: '/blog',
name: 'blog',
component: Blog,
children: [{
path: 'blogdemo',
component: Blog
}
]
}]
export {
routes
}
// 注意
/* webpackChunkName: "blog" */
//是为了后面的路由懒加载,后面会讲,不懂没关系,现在可以忽略
- 自动生成的 index.js 主路由是这样的
- 缺点:
太单一,我们不可能所有的路由都写在里面作为 children ,看起来非常混乱,开发调试很难处理,多人协作还很容易引起冲突。
我们尝试将 blog.js 引过来
先将 blog.js export 的路由引过来,起一个别名防止冲突
import { routes as blogRoutes } from './blog'
因为可能有N多个路由模块,我们将 routes 拆分
自动生成的是这样简单的:
//直接导出路由
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
我们拆分成这样:
//定义基础路由
let route = [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
//以此类推可以方便的链接更多路由
route = route.concat(blogRoutes)
//导出
export default new Router({
routes: route,
linkActiveClass: 'active'
})
回过头来,我们为 blog/index.vue 添加一些内容,测试一下:
<template>
<div class="Blog">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: "Blog",
data() {
return {
msg: "Welcome to Your Blog Page"
};
}
};
</script>
测试一下
首先
npm install
嫌慢可以使用淘宝的 cnpm 我以前的文章讲过
npm run dev
按照提示在浏览器输入:http://localhost:8080/#/blog
vue-cli2 webpack 生成的项目是支持热部署的,所以很多配置不需要自己从零开始配置,这也是我希望大家使用 CLI 的原因,省去了一些时间。其他 login 等一些模块 按照这个模式写就可以了。
谈谈 eslint
个人觉得 esLint 不论是在个人项目还是团队协作中,都是有价值的,它可以让自己、团队的代码风格标准化。现在esLint 甚至可以预测你的代码是否可能会有问题。建议可以制定一些规则,开发时通过你的 IDE(集成环境) : idea 、WebStorm、 vscode、 之类的插件配合检测,eslint 打包检测编译不通过的那种非常严格的初期还是不要尝试了。
我当时参考 airbnb 调整的配置,经过一年多的项目实战逐渐调整,目前还算比较合理,esLint配置规则代码在这里。
本章代码在这里
你甚至可以再 commit 里看到本章循序渐进的改造过程
Part 6 打磨前端架构
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 加载动画
axiosIntercept.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-m...
请求一下,不出意外浏览器做了跨域报警。
哦,我们适配一下 node 代理
官方例子在这:
https://vuejs-templates.githu...
扩展一下 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;
});
}
Part 7 快速构建网站布局
本文为方便讲述重构去除了 Element、vux 库,用了最近比较火的 bulma 轻量、快捷、易读。
项目截屏
Layout and Components
Layout
首先,似上图,我们思考把一个小型网站拆成三部分:页头(Header)、内容(Content)、页脚(Footer) 这几乎每个网站内都必须有的,通常把万年不变的:页头(Header)、页脚(Footer) 制作成 Layout 方便通用。
Components
再把内容(Content)根据业务进行拆分成 组件(Components)
如上图:Header 和 Content :Header其实没有拆分的必要,没有可以重用的组件,而 Conntent 是必须要拆分的布局元素。因为动态网站 Conntent 随着内容的变化而变化,内容多,可重用的东西的概率越高,需要把能重用的东西提炼出来
1、节省代码、提高代码阅读性
2、便于修改 (比如更新广告)
开始写代码
接着我们的 第二章上传的源码 开始,基于它继续完善小网站布局和组件化。
值得一提的是:本系列教程一章跟随一章并且每一章可以独立运行,章与章之间完美衔接,没有 “突然出现” 的代码段。不会给新手无从下手的感觉,如果那你对代码陌生,那你该认真翻翻往期文章了。你可以基于上一章逐步写代码,也可以下载本章简单预览代码。
- 引入 bulma 样式 CDN
vim new-bee/index.html
<!-- font -->
<link href="//cdn.bootcss.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
<!-- css -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.0/css/bulma.min.css">
- 新建 Layout 目录
vim new-bee/src/renderer/components/layout
- Layout 目录下建立 Header.vue 模板组件
这个组件专门写头部的内容,最好配合栅格尽可能写出简单的响应式组件
<template>
<div id="bee-header" element-loading-text="正在努力请求github..." element-loading-background="rgba(0, 0, 0, 0.8)">
<!-- 遮罩 -->
<div :class=" loading ? `modal is-active` : `modal` " style="background-color: #ffffff36">
< img src="https://img.actd.tw/images/2018/11/17/ez-4-05f4bba41fef" style="width: 300px" alt="">
</div>
<div class="is-underline ">
<div class="container">
<nav class="navbar ">
<div class="navbar-brand">
<a class="navbar-item" >
< img src="https://img.actd.tw/images/2018/11/17" alt="Bulma: a modern CSS framework based on Flexbox" width="92" height="28">
</a >
<div class="login-before is-hidden-mobile" style="padding-top: 5px;">
<a class="navbar-item is-hidden-desktop" href=" " target="_blank">
<span class="icon" style="color: #333;">
<i class="fa fa-lg fa-github is-size-2"></i>
</span>
</a >
</div>
<div class="navbar-item is-hidden-desktop ">
<div class="field has-addons" ><div class="control" ><input type="input" class="input" name="email" placeholder="搜索一下" required="required" style="height: 36.4px;width:130px"><input type="hidden" name="redirect" id="name" value="/fr/#thanks"></div><div class="control" ><input type="submit" class="button is-warning" value="GO"></div></div>
</div>
<div class="navbar-burger burger" data-target="navMenuDocumentation" >
<span></span>
<span></span>
<span></span>
</div>
</div>
<div id="navMenuDocumentation" class="navbar-menu">
<div class="navbar-start">
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link is-active">
发现
</a >
<div class="navbar-dropdown ">
<a class="navbar-item " type="收藏集">
收藏集
</a >
<a class="navbar-item" type="徽章">
徽章
</a >
<a class="navbar-item " type="排名">
排名
</a >
<a class="navbar-item " type="职场生活">
职场生活
</a >
</div>
</div>
<a class="navbar-item " href="https://bulma.io/expo/">
<!--<span class="bd-emoji">⭐️</span>-->
专栏
</a >
<a class="navbar-item " href="https://bulma.io/expo/">
<!--<span class="bd-emoji">⭐️</span>-->
聊天
<!-- 很多人不知道干什么。。。 -->
</a >
<a class="navbar-item " href="https://bulma.io/expo/">
<!--<span class="bd-emoji">⭐️</span>-->
面经
</a >
<router-link class="navbar-item " to="/book">
<!--<span class="bd-emoji">❤️</span>-->
书籍
</router-link>
</div>
<div class="navbar-end">
<div class="login-before" style="padding-top: 5px;">
<!-- pc -->
<a class="navbar-item is-hidden-desktop-only" href="https://github.com/pkwenda/my-bbs" target="_blank">
<span class="icon" style="color: #333;">
<i class="fa fa-lg fa-github is-size-2"></i>
</span>
</a >
</div>
<div class="navbar-item is-hidden-mobile ">
<div class="field has-addons" ><div class="control" ><input type="input" class="input" name="email" placeholder="搜索一下" required="required" style="height: 36.4px;"><input type="hidden" name="redirect" id="name" value="/fr/#thanks"></div><div class="control" ><input type="submit" class="button is-warning" value="GO"></div></div>
</div>
<div class="navbar-item is-hidden-mobile ">
<!--<span class="icon is-medium">-->
<i class="iconfont icon-tixing"></i>
<!--</span>-->
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="is-hidden-mobile" target="_blank">
< img src="https://avatars2.githubusercontent.com/u/14212375?s=400&u=dc515636befebfda36501309d1cdc087ee31d500&v=4" class=" header-avatar img-circle "
style="margin-top: 10px">
</a >
<div class="navbar-dropdown ">
<a class="navbar-item " type="收藏集">
写文章
</a >
<a class="navbar-item" type="徽章">
设置
</a >
<a class="navbar-item " type="排名">
退出
</a >
</div>
</div>
<div class="login-before">
<div class="navbar-item">
<div class="field is-grouped">
<p class="control">
<a class="button is-warning" v-show="!isLogin" >
<strong>登录</strong>
</a >
</p >
</div>
</div>
</div>
</div>
</div>
</nav>
</div>
</div>
</div>
</template>
<script>
export default {
name: "BeeHeader",
data() {
return {
popupShow: false,
isLogin: false,
user: {},
loading: false,
userInfo: {}
};
},
created() {},
destroyed() {},
mounted() {},
methods: {}
};
</script>
<style scoped>
.img-circle {
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
}
</style>
什么样式可以写在 .vue 文件中
上文的比较熟悉的代码是让我们的头像变圆的代码段
<style scoped>
.img-circle {
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
}
- 效果
这里我偷了个懒,刚好可以说一说,对于如此通用的样式,局限在 .vue文件中,并且以 scoped 标示,宣判了它无法复用的事实,任何模块想用这个样式,都需要复制一份,显然是不规范的,我们通常还会建立通用的 css 文件进行管理,大型项目 css 管理规范将更加严格、规范的树级结构,具体就看 CTO 的想法了。
根据喜好选择如何布局
按需引入
vim new-bee/src/renderer/components/HelloWorld.vue
<template>
<div>
<Header></Header>
<!--<div class="container"> </div>-->
</div>
</template>
<script>
import Header from "@/components/layout/Header";
export default {
name: "NewBeeIndex",
components: { Header },
data() {
return {};
},
destroyed() {},
mounted() {},
methods: {},
watch: {}
};
</script>
缺点是要一个一个引入,但优点是代码可读性高
全局引入
- App 主入口
vim new-bee/src/renderer/App.vue
- 引入
<template>
<div id="app">
<Header></Header>
<!-- < img src="./assets/logo.png"> -->
<router-view/>
</div>
</template>
<script>
import Header from "@/components/layout/Header";
export default {
name: "App",
components: { Header }
};
</script>
<style>
</style>
- 查看效果
基于 webpack 爸爸的热部署,我们无需刷新浏览器,webpack 偷偷用 ws 更新了我们的内容。似乎很完美,但是也许大家发现了一个问题,我们通过浏览器渲染出来的 dom 就可以看到::
我们在主 APP 入口引入了头部布局, App.vue 是紧临 <body> 元素的正文元素,而这个程序所有页面、子路由全部都是 App.vue 入口的子集,说明全局引入布局会存在如下问题:
1、这个项目所有的项目都一定会带上 Header 组件渲染的内容
2、而且会影响在下期 《性能优化》中讲的 webpack 按需加载的性能。
当然可以再 Header 组件上书写逻辑条件,过滤指定的路由,但会破坏项目的易读性,难以维护我个人是比较推荐第一种:按需引入的方式。
继续布局
- 照猫画虎写好 Footer
vim new-bee/src/renderer/components/layout/Footer.vue
<template>
<footer class="footer footer-light-medium " style="padding-bottom: 20px;padding-top: 20px;">
<div class="container">
<div class="columns">
<!-- Column -->
<div class="column is-4">
<div class="mb-20">
< img class="small-footer-logo" src="https://img.actd.tw/images/2018/11/17/" alt="">
<div class="footer-description pt-10">
new bee 是一个为开发者提供的专注于技术分享的开源社区,所有源码均可在 github 上找到,希望对广大开发者有所帮助。
</div>
</div>
<div>
<span class="moto">喜欢项目可以点赞支持 <a href="https://github.com/pkwenda/new-bee" target="_blank">
<span class="icon"><i class="fa fa-github"></i></span>
</a >.</span>
<div class="social-links mt-20">
</div>
</div>
</div>
<!-- Column -->
<div class="column is-6 is-offset-2">
<div class="columns">
<!-- Column -->
<div class="column">
<ul class="footer-column">
<li class="column-header">
Links
</li>
<li class="column-item"><a href="https://github.com/pkwenda/new-bee">Home</a ></li>
<li class="column-item"><a href="https://cssninja.io/themes">Blog</a ></li>
<li class="column-item"><a href="https://github.com/pkwenda/new-bee/wiki">Wiki</a ></li>
</ul>
</div>
<!-- Column -->
<div class="column">
<ul class="footer-column">
<li class="column-header">
Ressources
</li>
<li class="column-item"><a href="https://cssninja.io/help">Help center</a ></li>
<li class="column-item"><a href="https://cssninja.io/blog">Blog</a ></li>
<li class="column-item"><a href="https://cssninja.io/help/rules">Rules</a ></li>
</ul>
</div>
<!-- Column -->
<div class="column">
<ul class="footer-column">
<li class="column-header">
Terms
</li>
<li class="column-item"><a href="https://cssninja.io/help/terms/licenses/personal">Personal</a ></li>
<li class="column-item"><a href="https://cssninja.io/help/terms/licenses/developer">Developer</a ></li>
<li class="column-item"><a href="https://cssninja.io/help/terms/service">Terms of Service</a ></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</footer>
</template>
<script>
export default {
name: "Footer",
data() {
return {};
},
created() {},
destroyed() {},
mounted() {
},
methods: {}
};
</script>
别忘了在 HelloWorld 引入一下
- 看看效果
- 看起来效果还不错,接下来是 Content(正文)部分
vim new-bee/src/renderer/components/layout/Content.vue
<template>
<div class="container" style="height:700px">
<h1 >博客列表</h1>
<article class="column is-3" v-for="blog in blogs" v-bind:key="blog">
<a class="bd-article-image is-bootstrap" >
<span class="bd-article-overlay"></span>
<span class="bd-article-icon">
<i class="fa fa-tag"></i>
</span>
<strong class="bd-star-icon" ><i class="fa fa-star"></i> <span style="font-size: 1rem"> {{blog.commendCount}}</span></strong>
<strong class="bd-article-info">
<span>
<time class="bd-article-date" datetime="2017-10-09T00:00:00+00:00">
{{blog.tag}}
</time>
<strong class="bd-article-title">
{{blog.title}}
</strong>
</span>
</strong>
</a>
</article>
</div>
</template>
<script>
let article = { tag: "java", title: "java", commendCount: 0 };
export default {
name: "Footer",
data() {
return {
blogs: [
article,
article,
article,
article,
article,
article,
article,
article
]
};
},
created() {},
destroyed() {},
mounted() {},
methods: {}
};
</script>
<style scoped>
.bd-article-image.is-bootstrap {
background-color: #6f5499;
}
.bd-article-image {
background-color: #00d1b2;
display: block;
height: 240px;
margin-left: auto;
margin-right: auto;
position: relative;
text-align: center;
}
.bd-star-icon {
font-size: 19.2px;
font-size: 1.2rem;
color: #0a0a0a;
opacity: 0.25;
bottom: 10px;
left: 30px;
position: absolute;
-webkit-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
-webkit-justify-content: center;
justify-content: center;
}
.bd-article-icon,
.bd-article-info {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
}
.bd-article-info {
padding: 20px;
}
a strong {
color: currentColor;
}
.bd-article-date {
color: rgba(0, 0, 0, 0.5);
display: block;
}
.bd-article-title {
color: white;
display: block;
font-size: 1.8rem;
font-weight: 700;
line-height: 1.25;
padding: 0 20px;
}
.bd-article-icon {
color: #0a0a0a;
opacity: 0.25;
}
h1 {
text-align: center;
font-size: 30px;
}
.column.is-3,
.column.is-3-tablet {
-webkit-box-flex: 0;
-ms-flex: none;
flex: none;
width: 25%;
float: left;
}
</style>
- 看看效果
- HelloWorld.vue 代码看起来是这样的
还算看得过去,我们继续参照图二
为 Content 制定 AD(广告) 组件。
vim new-bee/src/renderer/components/common/AD.vue
<template>
<div class="ad"><h1>澳门XX赌场上线啦</h1></div>
</template>
<script>
export default {
name: "AD",
data() {
return {};
},
destroyed() {},
mounted() {},
methods: {},
watch: {}
};
</script>
<style scoped>
.ad {
width: 150px;
height: 180px;
background-color: #ececec;
position: fixed;
right: 30px;
top: 80px;
}
</style>
- 别忘了在 Content.vue 引入一下
...
<AD></AD>
...
import AD from "@/components/common/AD";
export default {
name: "Content",
components: { AD },
...
}
- 看下效果
- 对比一下我们之前 sketch 画的草图
差不多完成了我们初步的构思
Part 8 前端性能优化
本节橄榄
- 前端需要了解的 docker 基础知识
- 部署前端项目到本地/外网服务
- 前端项目的 gZip 优化
- 了解 CDN 的重要性
- webpack 按需加载
- 图片的相关优化
- 如何分析项目依赖,方便针对性处理
- 如何减小 webpack 打包大小/速度
上线
我们通常在本地开发,本地环境和线上也并非完全一样,很多项目第一次上线几乎都会遇到本地开发无法复现的问题,可能是字体、样式的问题,也可能是webpack 编译的问题、甚至可能是本地的奇葩环境。所以 本地完美运行 ≠ 线上完美运行,我们需要 build 项目,模拟线上测试一下,看看是否可以完美运行,有问题可以方便及时作出调整。
准备
为了避免本教程污染大家本地环境,推荐大家安装一个docker,后期运维也会根据 docker 展开。
看到这个 Title :《准备docker》,没接触过的前端不要怂,装一个,勇于跨出第一步,不学习就是等死「点击这里了解 docker」
虽然 tomcat nginx apache jboss jetty
等等等等都可以作为 http 服务,本章以最常见的 nginx 展开讲述:
大白话介绍下 docker
docker 就是用更优雅的方法,做到了虚拟机的事情,并且做的更好,可编程管理集群。docker 启动容器,在容器内部运行你的环境,默认各个容器是互相隔离的,当然你可以通过 link network 关联容器,或者直接使用 docker-compose 编排,启动容器的前提是镜像,也类似与虚拟机的镜像,想跑容器,先得下载「pull」镜像。
使用 docker
也许很多人没用过,没用过也不讲怎么安装了,自己去看官网吧中文官网、社区版下载、中国镜像加速,windows 的话可能要开启虚拟化,linux 推荐 ubuntu, 有篇文章这样讲:为了性能请不要在 centos中运行 Docker , 查看翻译点这里),几年前的文章了,现在怎么样有待考究。
看下 images 状态
docker images
可以看到我已经有一些镜像了「我已经删除了nginx」
dockerHub 拉 Nginx 镜像
docker pull registry.docker-cn.com/library/nginx:latest
正常 docker pull nginx 即可,中间那段是中国镜像源
ok,我们成功 pull 下来了 Nginx 的镜像。默认存储的镜像名为: registry.docker-cn.com/library/nginx
打包
进入我们上一章源码的目录,build 一下进行发布。
上一章源码在这里
npm run build
启动 docker 容器
docker run --name nginx -d -p 8888:80 -v /new-bee/dist:/usr/share/nginx/html registry.docker-cn.com/library/nginx
- 上调命令一些解释「不多讲,避免消化不良,自己探究」
CMD | 解释 |
---|---|
-d | 守护进程运行 |
-p | 端口映射 8888 :80 docker80端口映射到本机「宿主机」 |
-v | 挂载宿主机的一个目录 本机「宿主机」: docker容器 |
—name | 为容器命名 |
测试一下
http://localhost:8888/#/
当然初次尝试 docker 你可能会有更多的疑问:
- 你怎么知道需要将主目录挂载到: /usr/share/nginx/html ?
- 能否/怎样 查看 Nginx 日志 ?
- 容器内的 nginx 能否自定义配置 ?
- ......
这些小白问题本章简单讲讲,后面做自动运维的时候单独展开讲,可以关注我的博客
gZip
我们可以通过 webpack 压缩脚本文件,上传到 http 服务器,浏览器浏览的时候,经过压缩的HTTP应答报文是由浏览器解压的,比起压缩,解压的速度是非常快的(只要数据正常,可以解压的话),所以不用担心浏览器用于解压的时间会降低用户体验。事实上,浏览器解压消耗的这点时间比起数据包因为网络拥堵而耽误的时间要少的多也可控的多。
在浏览器发给服务器的HTTP请求报文中,使用Accept-Encoding字段标明自己支持的压缩格式,即自己可以解压哪几种压缩报文(gzip、zlib库提供的deflate)。服务器回复客户端的HTTP应答报文中,使用Content-Encoding字段标明该应答报文使用哪种压缩方式。
gZip 攻破 webpack、nginx
像我这样屌丝的服务器一般都买 1M 的,大的资源文件 hold 不住,一个动辄 400K 的 vendar 文件这很蛋疼,不上 gZIp 很难受。
打开 network 观察一下:
它有 144K 这么大
我们就以 webpack 打包的核心 vendor 为例,我们发现,客户端向服务端请求了 gZIp 资源 Accept-Encoding: gzip, deflate
,但可惜服务端并没有给我们理想中的 response - Content-Encoding: gzip
的响应, 我们需要排查一下原因。
- 首先看看 webpack 到底打没打出来打出来 gZip 呢?看看他的目录有没有 js 的 .gz 文件。
很遗憾没有,只有一些压缩文件和用于定位的 map 文件,看来首先我们的打包就出现了问题。
大家还记得当初构建项目我发的这张图吗?
- package.json 项目描述文件
打开看看 build 命令执行了哪个脚本?
打开 build.js 看看执行了哪些内容,难道是 vue-cli 没有为我们配置好webpack gZip 相关的配置吗?
我们发现没什么特别的,发现一个const webpackConfig = require('./webpack.prod.conf')
的依赖,大概就是字面意思(webpack生产配置)进去看看。
哦,我们看到了,webpack 确实为我们配置了 gZip 相关配置。
可是发现这个配置被这个判断包裹住了:
if (config.build.productionGzip) {
}
追踪下去
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
我们的全部疑惑都被揭开了,开发者通过注释这样告诉我们他的理由,我简单翻译一下:
首先下载一下依赖:
vim package.json
"devDependencies": {
"compression-webpack-plugin": "^1.1.12",
}
然后 productionGzip 改成 true
- 废话不多说打个包试试:
npm run build
成功了,出现了 .zg 文件压缩包,但是 gZip 是需要服务端的支持的,服务器通过客户端请求的 Accept-Encoding
首部开判断返回哪种格式的脚本文件,然后由浏览器解压,我们拉下来的 nginx 镜像,nginx 是不会为我们默认配置 gZIp 服务端压缩的,我们去查看一下吧。
进入 docker 主机
docker exec -it nginx /bin/bash
或者
docker exec -it nginx "bash"
CMD | 解释 |
---|---|
exec | 进入docker容器 |
-i | -i: 以交互模式运行容器,通常与 -t 同时使用; |
-t | -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用; |
-it | -it = -i -t |
“bash” 或 /bin/bash | /bin/bash的作用是因为docker后台必须运行一个进程,否则容器就会退出 |
进入 nginx 主机的第一件事
nginx 在哪???
Linux whereis 命令用于查找文件。
该指令会在特定目录中查找符合条件的文件。这些文件应属于原始代码、二进制文件,或是帮助文件。
该指令只能用于查找二进制文件、源代码文件和man手册页,一般文件的定位需使用 locate 命令。
语法
whereis [-bfmsu][-B <目录>...][-M <目录>...][-S <目录>...][文件...]
- 查看 nginx 位置
root@e0017cab245f:/# whereis nginx
nginx: /usr/sbin/nginx /usr/lib/nginx /etc/nginx /usr/share/nginx
- 我们在 /usr/share/nginx 找到了根目录 html/
- 我们在 /etc/nginx/ 找到了 nginx 配置文件
ps 中间件这么多,谁记得住呢,记不住自己看看就行了不是吗?
- 查看一下 nginx 的配置
确实 gZip 真的没开启,被注掉了。
我们打开 gZip 的注释,并且防止服务端对 css 偷懒,我们一步到位加上几行经典配置。
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types application/javascript text/plain application/x-javascript text/css application/xml text/javascript application/json;
gzip_vary on;
nginx配置 代码在这里
如何修改 docker 内部的配置
就目前来看,你有两种方法可以选择:
- 直接 exec 容器进行修改,增加上述 gZip 代码段。缺点:若想重新基于镜像构建容器容器内的配置会丢失。(除非你 commit 镜像)
- 独立出配置文件,一劳永逸。
我们基于第二点在 new-bee/ 目录 同级 创建了一个目录 nginx/ 创建一个同名的 nginx.conf 文件。
nginx配置 代码在这里
- 先停止 nginx 容器
docker stop nginx
- 删除 nginx 容器
docker rm nginx
- 重新构建 nginx 容器
docker run --name nginx -d -p 8888:80 -v /new-bee/dist:/usr/share/nginx/html -v /nginx/nginx.conf:/etc/nginx/nginx.conf:ro registry.docker-cn.com/library/nginx
- 看看效果
http://localhost:8888/#/
为了避免浏览器加载刚才的 304 缓存,清除下浏览器缓存或进行隐身模式
已经奏效了。
- 看看大小压缩到多少
只有 50K 左右,压缩了 2/3 的大小,这对于大型项目来说,节省的不只是 100K ,甚至是更多,webpack 或者说 gz 等压缩算法,会将所有的大量重复的片段单独标记,所以重复的越多,压缩的越多,这对于现在带宽比金子贵的云服务来说是十分重要的。
CDN
大家注意到,有些能用 CDN 的我选择使用了 CDN,那么 CDN 对于线上服务来说到底有多重要呢?
原理
请求速度
废话先不说给大家上个对比图 测试地址
- 这是我的 论坛
可以看到仅有几个地方还算不错,其余地方都是一塌糊涂
- 这是淘宝
不用说了吧?不过还好,这部分我们资金不足败了也很正常,但大家可能也大概知道 CDN 的意义了,主要意义不是节省开源项目服务器带宽,而是全国各个节点的访问速度问题,也就解释了:我部署的项目访问速度还不错,你这里怎么这么慢,你网不好吧?CDN 来告诉你答案。
cookie
我们还是拿实战的 bbs论坛 举例子吧,查看网络状态:
使用 CDN 的几点优势
- 访问快
- 服务端压力小
- webpack打包小且快(下面讲)
客户端的 cookie 是绑定服务端 域名 的, 看上图,我们需要 XHR 请求携带 cookie 访问服务端获取对应权限,但试想一下:每一个 js、img、甚至是css 都携带垃圾的 cookie ,在大用户量下,服务端承受着不应该属于他的痛苦,这样的消耗是特别应该避免的,我们可以随便翻一翻任何一个成熟的网站,都会发现存在自己的 CDN 服务,这样既优化了中国不同地区的访问速度,同时也大大减小了服务端的开销。
节省 webpack 打包大小/速度
很长时间前经历过公司前端 webpack 编译特别慢的问题,dev 模式下我们可以注掉开发范围外的 路由,但是 build 发布的时候似乎没法解决,使用了 Happypack 多线程打包还是不如人意,查阅资料读到了 这篇文章
我们可以把能够 externals 调的排除掉,然后使用 webpack 的 webpack.DllPlugin 生成依赖库(这点很重要),大大减少便以速度,DllPlugin 本质上的做法和我们手动分离这些第三方库是一样的,但是对于包极多的应用来说,自动化明显加快了生产效率。
如何分析项目依赖
webpack-bundle-analyzer
其实很多人都知道,可能刚入坑的同学不太了解,不管是 npm maven 都有自己一套以来分析工具,当然也都来源于第三方,这里为大家介绍 npm 的以来分析工具: webpack-bundle-analyzer ,他会在浏览器生成一个报表,直观的展示哪里大,哪里需要优化,以及预测 gZip 的大小,还是以 实战项目为例:
按照官方指引的配置,下载依赖,package.json 文件指定下 build 的脚本:
"analyz": "NODE_ENV=production npm_config_report=true npm run buildProd",
运行一下:
npm run analyz
效果:
分析:
发现了问题,static/
静态文件下 hightlight 文件比较大,有钱可以考虑下 CDN,node_modules/
下 element-ui 饿了么组件比较大,(我比较懒,全局导入的,可以用哪个引入哪个避免全局打包问题)可以优化,然后无聊的同学没事儿点点玩玩吧。
webpack 按需加载 : 一切皆模块
当打包构建应用时,Javascript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。 Webpack 的代码分割功能, 实现路由组件的懒加载.
官方说的挺详细了,这里就偷个懒不上代码了,给大家提供一种经典处理方式,我们不放在组件上,直接对路由进行拆分,具体可以看 实战项目路由 的路由拆分
会发现很多这种注释:
const Blog = () => import(/* webpackChunkName: "blog" */ '@/container/blog/Blog')
那么类似:
/* webpackChunkName: "blog" */
不是白写的,他是配合 webpack 对项目各路由拆分的,我们可以看看 实际项目加载情况 :
这个 blog.hash.js
不是我们写的,是 webpack 进行分割的,这样类似 vue 这样的单页面架构,不会加载某模块总是加载全部脚本,大大提升加载速度。
图片处理
本来不想讲的,简单说说吧,常用的也就那几种 svg 、base64、 或使用fastdfs组件类似 CDN 的服务。
base64
简单来讲 base64 会减少你的 http 请求数量,要知道 XHR 可不是省油的灯,他会带来额外的处理请求和处理响应损耗,以表情为例,动辄几十个表情 http 请求似乎太智障了一些,通常采用 base64 处理,减少了 http 请求数量,但是增大了图片本身的体积,如果你用了webpack 且你的表情在本地,那么 webpack 可以帮你自动进行 base64 编码哦。
压缩图片
用户上传的图片可以通过压缩图片大小或质量减少带宽哦,通常使用 GM 对用户上传的有必要大锁的图片 压缩成不同大小的,根据业务加载,比如头像,默认肯定不会请求原始图片,今日头条的正文,使用流量的情况下也会默认加载小图,这些都不是客户端能做到的,需要服务端压缩。
结语
当然这些知识万里长征的第一步,以后的优化之路茫茫多,能大概想起来的比如 :Lazy-Load(优化首屏体验)、PWA(构建web APP)、服务端渲染(为了SEO)、骨架屏(提升用户体验),后端和服务端文章还没写, 1.0 版本就放这些吧,期待第二版填坑,下篇开始后端。
更多推荐
所有评论(0)