本文为《了不起的JavaScript工程师:从前端到全端高级进阶》、《前端开发必知必会:从工程核心到前沿实战》和一些优秀文章的笔记,这两本书可以内容不够深入,不过可以查漏补缺、扩宽视野。
了不起的JavaScript工程师
JavaScript是语言工具,是“术”;但是如果能在熟悉JavaScript之后进行总结归纳、建立自己的学习模型、通过模型来快速掌握其他高级语言,那么这就是“道”。
“术”与“道”的关系就好像实践与理论的关系,两者相辅相成。精通“术”之后可以明白“道”,懂了“道”之后又可以用于“术”。前端的知识点其实很多,也很细碎,对于开发者而言,并不需要把它们都记住,而是要形成自己的知识框架,然后不断地去丰富它。
开发环境
提升开发效率的工具
IDE
编写代码的工具一般称作IDE(Integrated Development Environment,集成开发环境)。微软推出的Visual Studio Code,大品牌、跨平台、教程全、轻量快速、插件丰富、支持Vim模式、友好的Node.js 调试功能等是其特点。
Docker
Docker 是一个跨平台的开源容器引擎,可以用它来构建、管理、运行容器。
- 容器网络
安装好Docker之后,默认会创建3个网络。可以通过命令“docker network ls”进行查看 - 网络类型
1)bridge
如果未对容器的网络类型进行定义,默认将连接到此网络。它会为容器分配一个子网IP,通常是以“172.17.0.x”的形式分配。具体IP我们可以通过“docker inspect”命令进行查看。
2)host
此网络类型代表容器共享主机网络,(Linux下使用主机IP,Windows下创建一个虚拟IP,只有 主机能访问),直接占用主机端口,不再需要“-p”参数来指定端口。
这种方式的好处就是不再需要配置映射端口,但是容器和主机之间的网络隔离性消失,所以一般不使用此网络配置。适合代理服务器这种类型的容器进行使用。
3)none
当容器属于此网络类型时不会分配IP地址,其他容器和主机都无法通过网络访问它,在网络上它是完全与外界隔离的。
4)自定义 bridge
一个基础的Web应用,包括前端、后端、数据库3个容器。比较好的方式是自定义一个bridge网络,给这3个容器使用,一方面这3个容器在一个网络中,可以互相访问,同时对于这个网络之外的进程而言也是隔离的,外部进程只可以通过映射端口的方式让主机访问它们。 - 网络命令
创建网络,#:docker network create <网络名称>
。查看网络列表的命令前面已经提到过,即“docker network ls”。
通过这条命令可以查看到该网络网关、所连接的容器等重要信息,#:docker network inspect <网络id 或名称>
。
当容器被删除之后,给它配置的网络并不会随之删除,需要手动执行如下命令进行删除:#:docker network rm <网络id或名称>
。
通过下面这条命令来清除未被容器使用的网络,当然默认的3个网络不会被清除。#:docker network prune
。 - 容器存储
Docker提供了3种挂载容器数据的方式:存储卷、绑定挂载、tmpfs挂载。
1)存储卷:通过Docker来创建一个卷并以文件的形式存储于主机磁盘中,非Docker进程无法修改这个文件系统。这是Docker官方最推荐的存储数据方式。主要通过“docker volume”命令操作卷,只能在创建容器时指定卷与容器内部目录的对应关系。
2)绑定挂载:直接让容器与主机共享文件或目录,它可能被存储于主机的任何位置或者被任何进程修改。
3)tmpfs挂载:只存储于主机的内存中,不会写入主机文件系统。
通过Dockerfile构建镜像是有缓存的,每一条命令都会产生一个缓存层,当该命令和命令涉及的文件未变化时缓存即可以使用,从而缩短时间。一旦某个缓存层失效,那么后面所有命令都不会再使用缓存。所以我们在不影响正常执行逻辑的情况下,可以将不常变动的命令写到前面,这样可以加快之后每次构建镜像的速度。
Docker Compose是一个用于定义和运行多容器Docker应用程序的工具。它允许用户通过一个简单的YAML文件来配置应用程序的所有服务,并一次性启动所有服务。
每个服务都有一组配置选项,包括容器运行命令、端口映射、环境变量等。
Docker Compose的优势
- 简化了多容器应用程序的部署和管理。
- 提供了一种一致的方式来配置和运行应用程序的所有服务。
- 支持服务间的依赖关系管理。
一个小型的单机编排工具,通过编写YAML格式的配置文件,就可以通过“docker compose”命令来操作多个容器。当容器数量变得非常多以至于需要部署到多台服务器时,我们需要将这些服务器划分成集群,使用Kubernetes这类更强大的编排工具,它可以帮我们跨主机来部署容器,从而更合理地分配资源。
代码管理
GitHub Flow、Git Flow、GitLab Flow。
参考文章:Git 工作流程 https://www.ruanyifeng.com/blog/2015/12/git-workflow.html
其他软件
- Git Extensions
Git Extensions是一个用于管理Git存储库的独立UI工具,https://gitextensions.github.io/ 。 - TreeSize
TreeSize 是一款专门用于帮助用户管理磁盘空间的软件。它可以扫描您的硬盘,分析和显示您的文件和文件夹的大小,帮助您快速找到占用空间过多的文件和文件夹,从而清理出更多的磁盘空间。 - ExTab
ExTab(Explorer Tab)是国人开发的一款多标签资源管理器/多标签文件管理器/多标签 我的电脑,可以像浏览器一样用多标签管理每个文件夹,以便更加 快速高效的切换文件夹。2020 年 7 月才发布,更新也较为频繁。安装包只有 2.8M(2.0.0.8版本)。 - 快速搜索工具
Everything是一个全局的快速文件(夹)查找工具,可以单独使用,但是笔者更多的是让它配合Wox使用。Wox是一个通过键盘来快速启动应用和搜索网页的工具。 - 命令行增强工具cmder
cmder是一款Windows环境下非常简洁美观易用的cmd替代者,它是一个跨平台的命令行增强工具,可以集成windows batch, power shell, git, linux bash等多种命令行于一体,支持了大部分的Linux命令,比如 grep, curl(没有 wget),vim,grep,tar,unzip,ssh,ls,bash,perl等。还可以通过自定义,让它更方便。自动化构建工具
当页面资源类型变多,逻辑变得复杂时,我们需要对其进行管理,比如模块化、压缩、合并等。另外对于模板和预处理语言也需要编译生成浏览器可识别的文件,所以构建工具被广泛使用。常用的有Grunt和Gulp。
使用Gulp的优势就是利用流的方式进行文件的处理,使用管道(pipe)思想,前一级的输出,直接变成后一级的输入,通过管道将多个任务和操作连接起来,因此只有一次I/O的过程,流程更清晰,更纯粹。Gulp去除了中间文件,只将最后的输出写入磁盘,整个过程因此变得更快。
构建工具推荐使用Gulp,因为临时文件存储于内存中,速度较快,而且插件丰富,任务化定制,使用灵活。高效编写/组织代码的心法
- 拆分。
拆分就是把不同逻辑功能的代码划分到不同的文件,在代码执行的时候(或之前)通过依赖管理或工具来生成目标文件。抽象就是从具体问题中提取出具有共性的模式,形成可以公用的代码文件。
拆分的目的主要有:简化问题;方便阅读;优化性能。
按照下面的3种方式来对代码进行拆分。
1 按文件类型拆分
2 按功能类型拆分
3 按关注点拆分:这种拆分方式更加贴近代码功能,将功能上相关的代码划分到一起。 - 抽象
抽象的目的主要有以下几点:
●避免重复。
●减少问题。代码体积的增加会使项目的复杂度增加,从而更加容易出现bug。
抽象原则:
- 第一原则:DRY
“DRY(Don’t Repeat Yourself)”,这个原则主张项目中的每一段代码都必须是唯一的、明确的,不能含有任何重复代码,认为任何重复代码都应该被抽象出来进行复用。 - 第二原则:YAGNI
YAGNI(You aren’t gonna need it ),翻译过来的意思就是直到你确实需要某个功能时才添加它。这种只关注核心功能的方式,可以大大加快开发速度,同时代码越少一般bug也越少。 - 第三原则:The Rule of Three
就是当一段代码逻辑出现第三次的时候再去抽象出它进行公用。
代码规范能提升代码开发效率,好的设计模式可以提升代码执行效率。
模块
封装模块,这样做可以带来以下好处。
● 抽象代码:将复杂的或需要经常使用的功能进行抽象。
● 封装代码:如果我们不希望代码被更改,则可通过模块隐藏内部代码。
● 重用代码:模块化的代码更方便被引用,避免重复编写相同的代码。
● 管理依赖:在不重写代码的情况下轻松更改依赖关系。
ECMAScript是由ECMA国际制定的脚本语言规范,最早是根据JavaScript语言制定,后来又成为了JavaScript语言遵循的规范。2009年颁布的第5个版本也被简称作ES5,是目前使用最多的版本。
- ECMAScript5中的模块
- 立即执行函数表达式(Immediately-Invoked Function Expression)
- 显式模块声明
显式模块声明和立即执行函数表达式(以下简称“IIFE”)有些类似,相当于它的进化版。区别在于显式模块声明中将函数返回值分配给了一个变量。显式模块声明虽然相对IIFE有所优化,但是同样不支持依赖管理机制。所以它们都不是完善的模块管理规范。 - 异步模块定义
异步模块定义(Asynchronous Module Definition,简称AMD)是比较早的JavaScript模块管理规范,RequireJS 就是遵循AMD实现的用于浏览器端的第三方库。AMD的优点就是模块加载都是异步执行的,即可以同时加载多个依赖的模块,从而提升模块加载速度。其缺点就是依赖模块之间不是按照声明的顺序加载 - 共同模块定义
共同模块定义(Common Module Definition,简称CMD)是国产JavaScript库sea.js提出的懒加载的模块管理方式。它和RequireJS一样通过define函数来定义模块,通常一个文件就是一个模块,在定义模块的时候会给声明函数注入3个参数require、exports、module,require函数用来动态引用模块,exports 用来暴露模块接口,module提供当前模块的一些参数。需要注意的是,通过require函数加载模块是同步执行的,如果要实现异步加载那么得用require.asyne函数。
和AMD规范的RequireJS相比,CMD规范的sea.js不仅支持浏览器端,还可以在Node.js中使用。同时sea.js 也非常关注代码的开发调试,有nocache、debug等用于调试的插件。 - CommonJS
如果说AMD是专注于浏览器端的JavaScript模块管理规范,那么CommonJS就是专注于Node.js服务器端的模块管理规范。它是Node.js默认的模块定义方式,不需要额外引用第三方库。它规定一个文件就是一个模块,同时抛开了“函数”的束缚,不再需要通过define或者匿名函数包裹来实现模块定义。
Node.js中引用的模块都为本地文件,不存在异步的情况,所以模块的引用都是顺序执行的。 - 通用模块定义
通用模块定义(Universal Module Definition,UMD)就像是AMD和CommonJS的代理模式,实现起来也非常简单粗暴:先判断是否支持AMD,存在则使用AMD方式加载模块;再判断是否支持Node.js模块格式,存在则使用Node.js模块格式;前两个都不存在,则将模块公开到全局。
- ECMAScript6中的模块
ES6 的模块和CommonJS有些类似的地方,规定每个文件就是一个模块,模块内部的代码不能被外部访问。
ES6支持花括号的写法来导入导出变量。在ES5中,每次引用模块都是默认我们将会使用整个模块,也就是将整个模块进行加载,使用它的时候需要动态执行。而ES6允许我们可以只使用模块的某个变量或者函数。这种静态加载的方式使得遵循ES6的模块可以在编译时就完成模块加载,加载效率有所提升。同时更重要的是使得静态分析成为可能,让模块打包工具大展身手,同时还可以通过Tree Shaking等方式来优化编译后的模块。 - 模块打包工具
模块打包工具的职责就是将不同的模块,打包整合成新的(在浏览器端或Nodejs中)可识别模块,新的模块有可能是多个,也有可能是一个。
browserify是一个主动向Node.js靠拢的模块打包工具,它能处理浏览器端的遵循CommonJS 规范的模块。
parcel号称是非常快速的、零配置的Web应用打包工具。
webpack 是一个现代JavaScript应用程序的静态模块打包器(modulebundler )。当webpack处理应用程序时,它会递归地构建一个依赖关系图( dependencygraph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个包。JavaScript的几个趋势
● 静态化。
首先是数据类型静态化。TypeScript支持了数据类型的校验,避免一个变量被赋值多种数据类型而引起混乱。然后可以通过接口对函数的入参、返回值以及变量的数据结构进行自定义和校验。其次模块引用也是静态化的,还同时支持解构等操作,使得模块可以被部分编译加载。
● 工程化。
模块与命名空间的引用结束了浏览器端JavaScript长期没有统一模块管理的局面,从而可以更好地支持大型项目开发。
● 靠拢后端语言。
借鉴了其他热门后端语言的特性,比如Python中的装饰器、匿名函数,Java中的类。
● 前后端统一。
模块管理机制是为浏览器端和Node.js端设计使用,对全局对象的统一处理,都是在为JavaScript的同构铺平道路。HTTP请求优化
- 减少连接/请求数
从客户端来说,要取消那些重复的请求,不应该在短时间内多次发送重复的请求来获取资源,而应在前端抽象或者缓存。从API层面来说,对于某些请求我们可以要求服务器端返回冗余数据。在API的层面上还可以合并请求从而减少请求数量,但是这个方法通常是一把双刃剑,因为合并后的请求数量是减少了,但合并的请求会使耦合性增加,同时合并后返回的数据相对合并前也会有所增加。
由于HTTP协议是基于TCP协议封装的,所以在每次通信之前都会进行握手来建立连接,这样会略微消耗性能,大量请求积累起来的时间消耗就变得不容忽视了。所以建立TCP连接后保持不断开,在此连接之上重复传递数据是一种有效的方式,也是HTTP/1.1默认的实现方式。需要注意的是,保持长时间连接会加重服务器端的负担,所以合理地设置连接时间变得非常重要。 - 缓存数据
缓存通常是进行性能优化的最有效手段。使用HTTP协议的客户端和服务器端都可以进行有效的缓存。客户端可以使用主动缓存,例如浏览器将资源文件存储在localstorage中。也可以通过报文头部字段进行被动缓存,请求报文中携带上次响应返回的last-modified 头部或etag头部,若服务器端响应304状态码则使用本地缓存。服务器端这一块也可以缓存静态资源,比如CDN服务器就是在不同节点服务器上缓存静态资源,使用最近的节点给客户端响应请求。当然这不仅能提升响应速度,同时也可以缓解服务器的压力。 - 减少传输数据量
除了减少传输内容,主要使用的方式就是压缩技术,比如常用的gzip技术,就可以有效地压缩资源体积,gzip这类压缩技术需要服务器端启用,同时浏览器支持。当然更通用的方式是直接对文件进行压缩 - 优化网络链路
减少网页中域名的使用,就可以减少以上域名解析的过程所耗费的时间。另一种优化传输链路的方式是使用CDN(Content DeliveryNetwork)。
HTTP/2协议
- 多路复用
在HTTP/1.1协议中,为了避免浏览器过度消耗服务器端资源,所以限制了浏览器针对同一域名的并发请求数量,不同浏览器的限制数量在4~8不等,超过限制数量的请求将会被阻塞,延迟发送。HTTP/2相对于HTTP/1.1的第一个优势一多路复用。 - 压缩
HTTP/2启用了强制性的压缩算法来压缩报文。压缩的范围还包括HTTP/1.1不支持的响应报文头部。 - 支持TLS
HTTP/2中允许使用TLS1.2或更新版本的协议来进行加密,使得通信更加安全。 - 应用层协议协商
支持应用层协议协商(Application Layer Protocol Negotiation,ALPN),以便客户端能够从HTTP/1.0、HTTP/1.1、HTTP/2乃至其他非HTTP协议中做出选择。 - 服务器端推送
简言之就是在客户端发送请求时,服务器端可以自行判断,返回比当前请求更多的数据。这样可以有效减少客户端请求次数,从而在一次请求中得到渲染页面所需的资源,而无须在收到请求、解析页面后再发送新的请求。 - 流控制
流控制是HTTP/2提出来的一个新概念,目的是阻止发送方发送过量的数据,以免超出接收方的处理能力(接收方可能当前负载较高,无法进行处理,也可能出于某种原因想暂停传输)。简言之就是接收端可以告知发送端能接收的数据大小,发送端在发送时进行计算,发送不超过设定值的数据。HTTPS协议
HTTP 的缺点
- 通信使用明文
HTTP本身不具备加密功能,无法对通信整体进行加密,所以通信采用明文的方式发送。这样一旦网络被窃听,报文内容很容易被获取。所以为了保护信息不被解析,当然就是进行加密,针对加密对象的不同,可以分为通信加密和内容加密。 - 不验证通信方身份
虽然HTTP本身不提供验证通信双方身份的机制,但可以借助SSL的证书机制来实现。 - 无法证明报文的完整性
由于HTTP协议无法证明报文的完整性,如果中间方篡改了信息,通信双方都没有方法来确认信息的准确性。这样就容易遭受中间人攻击。保证消息的准确性通常可以使用数字签名的方法。虽然HTTP协议安全性方面无法保证,但HTTPS的问世解决了这些问题。当我们访问HTTPS网站的时候,可以很明显地看到浏览器地址栏前会有一个锁形的标记
HTTPS并非是一种新的协议,而是将HTTP协议的通信接口部分用SSL(Secure Socket Layer,安全套接字层,保障数据传输过程中不被截取和窃听的一种协议)或TLS(Transport Layer Security,安全传输层协议,用来确保通信双方传递数据的保密性和完整性)协议代替而已。HTTPS 通信流程
服务端发送SSL证书给客户端进行校验,校验通过后客户端生成随机密钥发给服务端,双方用该密钥进行加解密通信。密码学基础
密码学粗略地可以分为古典密码学和现代密码学。它们之间的区别很多,比如出现的时间、加密难度、使用的工具等,其中最核心的区别就是古典密码学的安全依赖于加密方式,现代密码学的安全依赖于密钥。
现代密码学大体上可以分为两种加密方式:对称加密(如AES、DES)和非对称加密(如RSA)。对称加密速度较快,加密和解密都使用同一个密钥。非对称加密会产生两个成对的密钥,用其中一个密钥加密后只能由另一个密钥解开。HTTPS 协议中使用的就是现代密码学中的两种加密方式。摘要与签名
常用的哈希函数有两种:MD5和SHA。这个通过哈希函数生成的字符串称为“消息摘要”,接收方只要将接收到的数据通过哈希函数进行计算,就可以得到一个消息摘要,然后和接收到的消息摘要进行比对,从而判断消息是否完整。
发送方会将生成的消息摘要加密,生成“数字签名”发送出来。只要接收方持有密钥即可对其进行解密,然后验证消息摘要内容。数字签名在消息摘要的基础上保证了消息的“不可篡改性”。X.509与证书
HTTPS 协议使用的是SSL数字证书(最早在SSL协议中使用),它遵循的是X.509格式标准。
客户端在拿到证书之后会将数字签名进行解密,并根据证书信息生成消息摘要,解密后的摘要与生成的摘要进行对比,如果一致则证明证书未被修改。然后校验主题、有效期、颁发者等信息。当这些校验都通过之后客户端就认定服务端是安全可信的。
SSL证书是由被称作CA(认证中心)的机构签发的,CA在收到制作证书的请求之后,会生成一对非对称加密算法的密钥,其中公钥被保存在新生成的SSL证书中,同时用CA自己的私钥进行签名后,将新生成的证书和私钥一起返回给申请者。申请者拿到证书和私钥后部署到对应的服务中即可。
SSL 证书在HTTPS协议中起到了两个作用:(1)身份认证,帮助客户端校验服务端身份。(2)SSL证书中的公钥用来加密传递数据(对称算法密钥)。WebSocket协议
WebSocket是HTML5引入的协议,它和HTTP协议之间存在一些微妙的联系。WebSocket协议和HTTP协议一样,都是建立在TCP协议之上的应用层协议,都属于可靠协议。WebSocket建立连接的时候需要使用HTTP协议通信。而WebSocket与HTTP协议的不同之处在于:WebSocket只需完成一次握手就能创建持久性连接,同时因为它是有状态的协议,不需要每次在发送报文的时候带上报文信息,可以直接发送数据内容,节省了一定的带宽。
WebSocket主要用于客户端需要监听获取服务器端数据更新的场景,例如浏览器端的聊天、监控页面获取实时数据以及调试工具的自动刷新。
对于服务器端来说是个考验一-要长时间保持连接。所以我们建立WebSocket连接的时候应尽量减少连接数量,如无必要,全局建立一个WebSocket连接来传输不同数据即可。Node.js概述
Node.js是一个用C++语言编写的JavaScript运行环境,从而让JavaScript开发者能对系统底层进行访问和操作。
Node.js具有以下几方面的优点。
● 强大的并发能力。它的事件循环机制使得Node.js在执行代码的时候并不会被耗时的IO操作(数据库查询、文件读写)所阻塞。
● 丰富的模块。
● 成熟的Web框架。
Node.js也有以下值得关注的缺点。
● CPU阻塞。
虽然I/O操作不会阻塞Node.js程序,但是CPU密集型的操作却可以。因为它是单线程,无法使用多线程来分担计算压力,所以一旦碰到CPU耗时的操作一样会被阻塞。
● 稳定性低。
单线程使得Node.js在稳定性方面不如多线程的服务器,因为一旦代码出现严重错误或者漏洞导致报错后,整个线程将无法继续工作,服务器端停止响应请求,但是好在有第三方模块如PM2等来帮助解决这个问题。
● 弱类型。
JavaScript的语言特性决定其不支持变量类型声明,甚至函数参数也可以不声明直接使用arguments来读取。目前最好的方式就是使用TypeScript来做静态类型声明和校验。
在Node.js执行的程序都是单线程的,单线程运行让它具有以下优势:● 减少了创建、销毁和切换线程的开销,执行速度相对更快;内存占用小;● 更加安全。多线程因为共享资源,同时读写同一资源的时候就容易出现问题,比如死锁。
当然单线程并不是万能的,它也有以下一些劣势。● 安全性不足。线程出错会导致整个进程退出,容错性不够。● CPU利用率。单线程只能利用CPU的一个核,如果CPU有多个核(大多数CPU都是如此),那么其他核的资源就浪费了。另外,如果遇到需要耗费大量CPU计算的代码,整个线程也会出现阻塞。再补充一点,单线程指的是Node.js运行我们编写的代码时是单线程的,Node.js的底层仍然是多线程的。
- 事件循环
在多线程的服务器端中,如果线程池中的线程都在处理请求,此时因为没有可用的线程,将不能处理新的请求。但是在Node.js服务器端中,如果底层线程池都被占用,有新的请求过来时,如果是非阻塞I/O请求,那么事件循环线程可以直接进行处理并返回结果。
大部分情况下在浏览器端的JavaScript只在脚本引擎的单线程中运行,开发者不需要也无法考虑线程、进程级别的问题。 - I/O操作
I/O操作可以分为网络请求和文件读写,其中文件读写既包括服务器磁盘上的文件读写,也包括数据库的读写。
Node.js的I/O操作都是非阻塞的,也就是说API函数采用的是异步回调形式,但fs模块也提供了同步API函数。
Buffer类被引入作为Node.jsAPI的一部分,使其可以在TCP流或文件系统操作等场景中处理二进制数据流。
数据流(stream)在Node.js中,数据流通常是指把数据从一个位置转移到另一个位置时,并不需要等到所有数据都加载完成再进行处理,而是分割成一段一段进行处理,而这些被分割的数据我们称之为流(Stream)。这样的好处就是可以节省内存,不必缓存所有数据。
用来存储“等待数据”的地方我们称之为Buffer。通常Buffer会存在于RAM内存中等待进程进行处理。
Node.js不能控制数据到达的时间和速度,也不能控制进程处理数据的速度,它只能根据数据是否处理完成来决定什么时候发送数据。Node.js将会在内存中建立一个缓冲区用来存放这些数据,等到合适的时间将它们发送给进程进行处理。 - 数据库
把数据存储于数据库中相对于以文件的形式存储至少有以下几个优势。● 方便查找。在文件中如果要查找某个单词出现的次数,必须读取整个文件并对字符串进行正则匹配,而数据库中可以通过SQL实现,并且可以使用一些辅助手段比如索引,来提升查询性能。● 权限控制。文件的读写权限需要牵涉到系统层面的修改,难以控制。而数据库具有灵活的角色和权限系统,使用不同的用户登录时可以读写对应权限的数据。● 并发机制。多应用读写同一个文件时容易出现阻塞造成性能问题,而数据库有处理并发的机制。
按照是否使用SQL(StructuredQuery Language,结构化查询语言)来分为关系型数据库(也称“SQL数据库”)和非关系型数据库(也称“NoSQL数据库”)。其中关系型数据库常用的包括Oracle和MySQL等,非关系型数据库常用的有MongoDB和Redis等。Node.js内存控制
对于前端开发者来说,基本不需要关注JavaScript的内存使用情况,浏览器本身也不提供控制内存的方法,它会自动帮我们分配和回收内存。而在Node.js中的服务器端程序,对内存这类资源的使用则变得非常重要。
默认情况下V8引擎限制了内存的使用(在64位系统下不超过1.4GB,32位系统下不超过0.7GB),带来的直接后果就是对于大型文件的操作能力有所下降
如果一定要在内存中处理大型文件,使用node命令行参数--max-old-space-size或--max-new-space-size
(单位为MB)来指定堆内存中老生代和新生代的最大值。
需要注意的是,这种方式是静态的,也就是在启动Node进程之前我们必须预先指定好值,如果想在程序运行时动态扩增内存限制是做不到的。 - 内存管理
- 内存分配
通常情况下,V8引擎会将对象放入堆(堆是一种树形的数据结构)内存中,堆内存的大小并不是一开始就按照最大值进行分配的,那样会消耗大量不必要的内存。堆内存是随着已使用的内存空间而动态扩展的,只有当需要消耗内存的对象被创建且可用堆内存不够时才会再次分配,当堆内存大小达到限制时不再分配并出现错误提示。 - 内存回收
在不断分配内存的同时还需要将不再使用的对象占用的内存空间进行回收。通常自动内存回收机制对应了不同的算法,没有哪一种算法对于所有场景都是最优解,因为程序在实际运行中,对象的生存周期长短不一,通常一种算法只对特定的某种或几种情况有最佳效果。
V8引擎首先将已使用的内存分成新生代和老生代,新生代内存指的是新产生的对象,通常经历过一次以下的垃圾回收,而老生代则是指存货时间较长的对象,至少经历过一次垃圾回收。新生代内存回收采用的是Scavenge算法。简言之就是将新生代内存空间分为from(已使用)和to(未使用)两个部分,而回收from 部分内存时判断to空间大小和对象经历的回收次数来将对象升级为老生代或者放入to空间,回收完成后from 空间和to空间角色进行翻转。老生代内存回收采用Mark-Sweep和Mark-Compact算法。简言之就是将老生代中不再需要使用的内存做标记然后进行回收,此时再通过标记来进行内存碎片的整理。
- 内存泄漏
内存泄漏就是指程序在运行时没有及时释放不再用到的内存,从而导致内存占用不断增加。因为一旦发生泄漏,哪怕只是一个字节,程序在长年累月的运行中,占用的内存空间就会越变越大,最终造成崩溃。而内存泄漏这类潜伏期较长的问题排查起来也相对困难。
内存泄漏的常见场景。
- 缓存
在Node.js中,缓存会被放在堆内存的老生代中,长期得不到释放。使用缓存策略来限制缓存大小,当存储数据超出缓存大小时给出提示或者清除已有缓存。当然这种方式并不是最优方式,更好的方式是在超出缓存限制时结合缓存使用频率,优先清除那些使用频率较低的或较老的缓存数据。
当需要用缓存存储大量数据时推荐使用专业的工具来实现,比如内存数据库Redis、Memcached、h2等。一方面因为Node.js是单线程,各个线程之间无法共享缓存;另一方面是专业的工具拥有对应的缓存管理策略,缓存使用效率更高,对程序的稳定性也更有帮助,即使缓存进程出现内存泄漏而退出,我们的Node.js进程仍然可以选择不使用缓存继续稳定运行。 - 不断增长的数组
类似生产者一消费者模型。解决方式一方面可以限制消费者处理数据的时间,另一方面也可以限制数组长度。 - 重复的事件监听
重复地进行事件监听而没有注销。避免这种情况发生的一种方法是放到重复调用函数的外层避免重复绑定,如果一定要多次绑定,那么在使用完之后要记得移除绑定。
heapdump是一个比较热门的V8内存分析工具,它的基本工作原理就是对进程的堆内存使用情况进行快照并生成JSON格式的文件,然后结合Chrome 调试工具进行可视化分析。
heapdump 的优点有以下三个。● 使用简单。作为Node.js模块,直接通过npm就可以安装,通过require可以引用,也无须复杂的配置。● 代码侵入性小。不需要修改业务代码,只需要在入口文件中引入模块即可,这样在撤除快照功能的时候也相当方便,当然它还支持一个手动写快照的函数writeSnapshot,可以在程序运行的时候生成快照文件。● 具有可视化能力。通过与Chrome开发者工具结合,可以较好地展现堆内存使用情况,相对于其他一些显示文本日志信息的工具来说更加友好。
- 大内存处理
用一种静态的方式来使程序能处理大内存数据,即在启动命令参数中设置新生代/老生代内存最大值。当然还有动态的解决方式,这里介绍两种一stream 和Buffer。
- stream
stream模块和Buffer 一样,是Node.js的原生模块,相当于是全局变量,可以直接使用,当然也可以通过require引用。它继承自EventEmitter模块,具有可读写特性。利用stream处理大文件就是一种拆分的思想,把大文件拆分成流再进行操作。 - Buffer
V8引擎的内存限制并不代表是Node.js的内存限制。如果我们使用Buffer来存储数据的话,那么就可以绕开这个限制,因为Buffer使用的是堆外内存。Node.js多进程
因为在操作系统中,一个线程只能使用CPU的一个核,而现在基本上都是多核CPU,如果要充分使用CPU资源,那么需要把其他核都调动起来。其他语言进程可以通过多线程来实现,而Node.js进程只有单线程,所以只能通过创建多进程来实现。
创建多进程通常需要考虑下面两个问题。● 进程间的通信:进程之间如何发送消息、传递数据。● 多进程管理:如何控制各个进程的启停,获取进程的运行状态。
介绍三个常用模块的使用方法以及它们对上述问题的解决方式。PM2模块
PM2 的缩写是Process Manager 2,是一个Node.js模块。目前主要用于在生产环境中管理Node.js应用的进程,同时还带有负载均衡、日志输出等功能。它可以在Node.js应用崩溃时进行重启,从而保障应用的稳定性。
虽然PM2工具简单好用,但遗憾的是PM2启动的进程之间是无法直接通信的。PM2启动的每个进程都相当于是一个副本,属于并列关系。其中一种最常见的使用场景就是实现负载均衡。在编写代码时为了保证程序的可扩展性,可以将这类重要的状态信息保存在文件或数据库中,这样就能实现进程间的数据共享了。
PM2对进程的管理能力我们从下面三个方面进行介绍。
- 守护进程
使用PM2来启动Node.js应用的时候,PM2会启动一个名为God的守护进程,它负责管理我们的应用。不仅是启动和停止操作,同时也能在应用崩溃时第一时间进行重启操作。 - 启停进程PM2支持的常用操作有以下几个。start:启动进程。restart:重启进程。。stop:停止进程。。delete:删除进程。
可以通过name、id、pid这些参数来指定进程,支持多个参数的批量操作。
如果停止/删除某个进程,那么God守护进程不会再次启动该进程。
当我们执行pm2 init命令的时候,PM2会在当前目录下生成一个名为ecosystem.config.js 的文件。当我们执行pm2 start命令时会自动读取当前目录下的ecosystem.config.js文件,启动所有配置的应用并添加对应的环境变量,除非使用参数–only来指定某些应用。 - 部署进程
可以在ecosystem.config.js文件中配置生产环境所需代码仓库的信息,包括ssh用户名、密码、IP地址、仓库地址、分支、路径、安装脚本等,然后通过pm2 deploy命令来进行远程部署。同时还支持更新、版本控制等高级操作。cluster 模块
Node.js 的系统模块cluster。使用cluster模块创建的应用属于“主从模式”,也就是一个主进程负责管理其他子进程。
cluster 模块支持以下两种连接分发模式。
● 第一种方法(也是除Windows外所有平台的默认方法)是round-robin法。由主进程负责监听端口,接收新连接后再将连接循环分发给工作进程。在分发中使用了一些内置技巧防止工作进程任务过载。之前我们使用PM2创建的多进程就是这样一种模式。
●第二种方法是主进程创建监听socket后发送给感兴趣的子进程,由子进程负责直接接收连接。
在实际情况下,由于操作系统调度机制不同,会使分发变得不稳定。同时在socket事件发生时,多个进程同时被唤醒响应事件,最后只有一个进程执行任务,这种因进程竞争而引起资源浪费的现象叫作“惊群”。一种比较极端的情况就是8个进程中的两个,分担了70%的负载。所以更多情况下我们采用第一种方式。
cluster 模块的isMaster属性来判断当前是否为主进程以及创建子进程的fork函数。使用setupMaster函数配置子进程参数,然后通过fork函数创建。setupMaster函数可以接收一个JSON对象作为参数来创建子进程
- 进程通信
一般情况下,子进程间也不能直接通信,但是和主进程之间通信是可以的。通过cluster.fork函数创建子进程后,会得到子进程实例,调用该实例的send函数可以发送消息,同时子进程通过监听全局模块process 的message事件来接收消息。子进程往主进程发消息同样也是调用send函数和监听message事件,只是主体互换了而已。 - 进程管理
1.创建进程cluster.fork():主进程创建子进程的函数。
2.销毁进程 ●disconnect():使进程断开网络连接,如果该进程为服务器端进程的话则退出,否则只是不再监听网络端口。●killO:完全销毁进程。
3.进程事件 ●disconnect:当进程失去连接时触发。error:当进程出错时触发。exit:当进程退出时触发。●listening:当子进程开始监听端口时触发。●online:当子进程启动时触发。child_process 模块
- 基本使用
child_process 模块提供了4种创建子进程的方式,分别介绍如下。● exec/execSync:创建一个 shell 并在shell 上运行命令,当完成时会传入stdout和stderr到回调函数。execFile/execFileSync:类似exec,直接执行文件内容。●fork:通过指定文件创建一个新的Node.js进程,允许父进程与子进程之间相互发送信息。spawn/spawnSync:通过命令启动一个子进程。 - 进程通信
进程间通信还有个专业术语叫IPC(Inter-Process Communication )。我们可以简单地把IPC理解为一根管道,它由主进程创建,子进程在创建的时候通过读取环境变量获取IPC信息,从而连接到这根管道进行通信,通信的方式是双向的,发送方调用send函数,接收方监听message事件。
通过主进程创建一个socket对象,通过IPC发送给子进程,子进程使用接收到的socket对象而不是重新创建。这种句柄的传递在处理网络请求的时候就变得非常有用了,例如多进程之间共享端口。使用send函数时并不是所有变量都可以传递。在传递句柄的时候实际上传送的是句柄文件描述符,这个描述符是一个整数值。消息内容和句柄将调用特定的函数序列化为字符串,然后发送到IPC管道。而子进程接收到字符串后进行反序列化还原成对应的消息内容和句柄,同时触发message事件。
send 函数和 message事件虽然能解决Node.js子进程与父进程通信的问题,但是对于其他语言编写的子进程就无法使用了。所以更通用的一种做法就是通过标准输入/输出来传递数据。
在命令行执行程序的时候,操作系统会自动打开三个标准文件。标准输入文件(stdin),通常对应键盘;标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件对应的是终端屏幕。当父进程需要向子进程传递数据的时候通过stdin来写入,子进程传递给父进程则通过process.stdout或process.stderr写入,然后父进程监听对应的data事件即可。 - 进程管理
由于cluster 模块是基于child_process封装的,所以在进程管理上两者存在一些相似的API和事件。
销毁进程:● disconnect()。关闭父进程和子进程之间的IPC通道。● kill()。向子进程发送终止进程的信号,如果子进程未终止则会触发“error”事件。
进程事件:● disconnect。当父进程或子进程的disconnect函数被调用后触发。error。当创建/销毁子进程失败或与子进程发送消息失败时触发。● exit。当进程停止时触发。message。当收到子进程发送的消息时触发。● close。当子进程的标准输入输出流关闭时触发。
worker_threads模块
从Nodejs 10.5.0开始,官方悄悄地新增了一个实验性功能一一创建真正的多线程。这个功能是专门用来处理CPU密集型任务的,在处理I/O任务时并不建议使用,因为可能会降低执行效率。
相对于前面提到的创建多进程的方式,worker_threads有下面几个优势。
● 操作系统切换线程比切换进程更快,开销更小。
● 线程之间可以共享内存。
在处理大型数据的时候就变得非常有优势。
● 线程共用父进程ID,方便管理。
- 基本使用
worker_threads模块创建子线程的过程和cluster模块创建子进程的过程在写法上有些类似,也是通过一个变量来判断当前是否是主线程,然后执行对应的逻辑。
worker_threads模块为了数据交互,向子线程提供了parentPort对象来访问父线程及workerData变量来接收父线程传入的数据。 - 线程通信
父线程在子线程初始化时,在workerData属性中传入数据,子线程通过workerData变量获取。子线程通过parentPort对象的postMessage函数向父线程发送消息,父线程通过监听子线程的message事件来获取消息。如果父线程希望动态地发送消息给子线程,也可以通过子线程实例的postMessage函数来进行传递。从代码结构上来看官方更推荐的是创建一个MessgeChannel类的实例来进行通信。 - 线程管理
销毁线程:terminate。立即停止工作线程,可以传入一个回调函数,在线程停止执行后触发。
线程事件:● online。当子线程创建成功后触发。● error。当子线程发生了一个未捕获的异常退出时触发。● exit。当子线程退出时触发。● message。当父子线程收到对方发来的消息时触发。● close。当父子线程的通信通道被关闭时触发。Node.js调试与测试
- 开发工具
nodemon 是一个工具,通过在检测到目录中的文件更改时自动重新启动node应用程序来帮助开发基于 node.js 的应用程序。123456--config 可设置指定配置文件--ext 设置监听文件的后缀扩展名,如想要监听ts文件的变更,需设置-e ts(文件后缀名)--exec 执行脚本(执行的命令)--watch 设置要监听的文件路径--ignore 设置无需监听的文件路径--verbose:设置日志输出模式,true为详细模式
这些参数也可通过配置nodemon.json文件来使用。
- 调试工具
编写代码时常用的调试方法有两种:日志调试和断点调试。
- debug 模块
只在开发过程中对指定代码进行日志信息输出,而在生产环境下不打印。第三方模块debug可以轻松地帮我们解决这个问题,这个模块可以同时在浏览器端和Node.js中使用,可以在代码中使用debug输出日志信息,而只有在开发时通过环境变量的控制来输出对应的日志信息,再不用在代码部署前清除日志信息了。
官方文档: https://github.com/visionmedia/debug 。 - Node-Inspector
日志输出只能解决一部分简单的问题,如果需要深入代码进行逻辑分析还需要借助更高级的调试工具,比如Node Inspector。这是Node.js自带的一个调试功能,在启动命令时添加参数–inspect指定调试地址端口,就可以通过相应的工具连接该地址进行端口调试。
开启端口先通过–inspect参数指定调试用的地址端口,我们指定本地地址0.0.0.0和1248作为调试端口:node --inspect=0.0.0.0:1248 app.js
此时控制台会给出日志信息表示Node Inspector已启用,有一个websocket连接可以使用,不过我们不需要直接使用。
使用Visual Studio Code的debug功能来进行调试:Visual Studio Code的左侧有一个debug菜单,单击之后出现调试窗口,单击齿轮图标对配置文件launch.json进行编辑。找到configurations属性,它的值是一个数组,添加一个JSON对象作为配置信息,1234567type: "node"request: "attach"为固定值,不需要修改。指定调试请求类型,通常为 "launch"(启动调试)或 "attach"(附加到已运行的进程)。name:配置名称。port: Node.js应用的debug端口。address: Node.js 的域名或IP。localRoot:本地源代码路径。remoteRoot:远程服务器端源代码路径。
配置完成后保存,在齿轮图标左侧下拉框中选中刚才的配置信息,然后单击绿色三角形开始按钮进行连接。这样我们就可以设置断点进行调试,并在调试过程中修改代码。只是每次修改代码之后需要重新启动Node.js应用,然后连接调试。
单元测试:
● 单元测试是保障代码的可持续迭代开发。
● 单元测试代码也能帮开发者思考清楚代码之间的逻辑关系。
● 单元测试保证了代码的可交付性。
编写单元测试的一个核心原则就是简单,或者可以称之为单一原则。一个单元测试案例通常只针对一个函数功能进行测试。单元测试文件通常也与源代码文件一一对应,然后加上后缀例如.test.js、.spec.js等,一个文件中可以包含多个单元测试案例。除此之外,也需要注意从正反两个方向来测试代码的可靠性,既要考虑符合预期的情况,也要考虑代码出现意外时的异常情况。
Node.js已经为我们提供了一个用于测试的assert模块,它可以实现基本的断言功能。这里的断言是指程序中的一阶表达式,即判断一个结果为真或假的逻辑表达式,目的是为了验证代码与开发者预期结果是否一致。断言为真时代表程序符合预期,否则抛出异常。
有一些框架既提供了模块也提供了命令行工具,比如ava。
为什么要用 AVA?
为什么不用 mocha,tape,tap?
Mocha 要求你使用隐式全局变量比如 describe 和 it 作为其默认接口(这是大部分人使用的)。这样做不是很好,并且串行执行测试没有进程隔离,使得测试十分缓慢。
Tape 和 tap 是非常好的。AVA 在它们的语法中得到大量启发,但它们也是串行执行测试,它们的默认 TAP 输出不是非常友好,因此你总是需要使用额外的 tap 报告。
与它们不同的是,AVA 可以并发执行测试,为每个测试文件提供独立进程,它的默认报告简单明了,并且 AVA 也支持通过 CLI 标志来输出一个 TAP 报告。
- 测试风格
主流的单元测试风格包括测试驱动开发(TDD)和行为驱动开发(BDD)两种。
测试驱动的开发方式更强调代码执行逻辑的正确性。好处就是代码可靠性比较强,可以有效地减少bug。但是缺点也比较明显,对于某些耦合度比较高的复杂逻辑,编写测试代码需要大量时间。行为驱动开发方式更符合人的直觉,从功能点出发来编写,有点像黑盒测试,不关注代码实现逻辑,更关注代码实现效果。 - 测试指标
通过率和覆盖率是单元测试最重要的两个指标。通过率一般要求100%,因此有一个测试案例执行失败则意味着测试不通过。覆盖率则要求不一,
普通的项目能做到90%也是非常不错的了。覆盖率相对于通过率而言更能体现代码编写质量,因为覆盖率达不到100%的代码很可能是代码本身在编写上有问题(比如耦合度过高)导致的。
“懂得避免问题的人胜过懂得解决问题的人”,测试代码就能很好地帮我们避免写出bug。前端开发必知必会
前端开发核心及Deno Web实战
package.json 文件
查看 npm 包的版本信息,以 vue 包为例。
查看最新版本:npm view vue version。
查看所有版本:npm view vue versions。
依赖包:npm 包声明会添加到 dependencies 或者 devDependencies 中。dependencies 中声明的是项目在生产环境中所必需的包。devDependencies 中声明的是开収阶段需要的包,如 Webpack、ESLint、Babel 等,用来辅助开发。要根据包的实际用途把它们声明在适当的位置。
若希望在找不到包或者安装失败时,npm 包能继续运行,则可将该包放在 optionalDependencies
对象中。optionalDependencies 对象中的包会覆盖 dependencies 中同名的包,这一点需要特别注意。
使用 optionalDependencies 的场景主要包括以下几个方面:
- 允许功能可选
- 提高跨平台兼容性
在跨平台项目中,某些依赖项只在特定平台上需要。使用 optionalDependencies 可以避免在不兼容的平台上强制安装这些依赖项,从而提高项目的跨平台兼容性。 - 降低安装失败的风险
使用 optionalDependencies 的注意事项:
处理依赖缺失:在代码中处理这些可选依赖项缺失的情况。可以使用条件导入或检查依赖项是否存在。
明确文档说明:在项目文档中明确说明哪些功能依赖于哪些可选依赖项,以及如何在需要时手动安装它们。
scripts 脚本:package.json 内置脚本入口,是 stage-value 键值对配置,key 为可运行的命令,通过 npm run 执行命令。除了运行基本的 scripts 命令,还可以结合 pre 和 post 完成前置、后续操作,该操作可以类比单元测试用的 setUp 和 tearDown。
main 配置:用来指定加载的入口文件,在 Browser 环境和 Node 环境中均可使用。
engines 配置:日常在维护一些遗留项目时,对 npm 包的版本或者 Node 的版本可能会有特殊的要求。如果不满足条件,则可能会出现各种各样奇怪的问题。为了让项目能够“开箱即用”,可以在 engines 中说明具体的版本号。需要注意的是,engines 仅起到说明的作用,即使用户安装的版本不符合,也不影响依赖包的安装。
bin 配置:许多包都有一个或多个可执行文件,可以使用 npm link 命令把这些文件导入全局路径中,以便在任意目录下执行。
lint-staged 配置:lint-staged 是一个在 Git 暂存文件上运行 linters 的工具,配置后每次修改一个文件即可给所有文件执行一次 lint 检查,通常配合 gitHooks 一起使用。
gitHooks 配置:定义一个钩子,在提交(commit)之前执行 ESLint 检查。在执行 lint 命令后,会自动修复暂存区的文件。
前端中的编译工具 Babel 7
它的功能有三个:一是转义 ES2015+语法的代码,保证比较新的语法也可以在旧版本的浏览器中运行;二是可以通过 polyfill 方式在目标环境中添加缺失的特性;三是源码转换。
Babel 中的 4 种配置文件
第一种是 babel.config.js
第二种是.babelrc,配置文件内容为 JSON 数据结构。
第三种是在 package.json 文件中配置 babel 字段。
最后一种是.babelrc.js,该配置与.babelrc 相同,但是需要使用 JavaScript 实现。
在这 4 种配置文件中,最常用的是 babel.config.js 配置和.babelrc 配置,Babel 官方推荐。babel.config.js 配置,因为该配置是项目级别的配置,会影响整个项目中的代码,包括 node_modules。有了 babel.config.js 配置之后,就不会去执行.babelrc 了。.babelrc 配置只影响本项目中的代码。
babel/core 包 是 Babel 的 核 心 包 , @babel/cli 包 和@babel/polyfill 包都需要在核心包上才能正常工作。@babel/cli 包是 Babel 提供的命令行工具,主要提供 Babel 命令。
- Babel 的工作过程
Babel 与大多数编译器一样,它的工作过程可分成三部分:
◎ 解析(parse):将源代码转换成抽象语法树(Abstract Syntax Tree,AST),树上的每个节点都表示源代码中的一种结构。解析过程可分为两部分:词法分析和语法分析。
◎ 转换(transform):对抽象语法树做一些特殊处理,使其符合编译器的期望,在 Babel 中主要使用转换插件实现。
◎ 代码生成(generate):将转换过的抽象语法树生成新的代码。 - @babel/polyfill 插件
该包会在项目代码前插入所有的 polyfill 代码,它带来的改变是全局的。。它实际上就是 core-js@2 + regenerator-runtime,core-js@2 也不推荐使用。
从 Babel 7.4.0 开始,这个包已经被弃用,取而代之的是直接包含 core-js/stable(以填充 ECMAScript 特性)。 - @babel/preset-env
babel 是通过 @babel/preset-env 来做按需 polyfill 和转换的,原理是通过 browserslist 来查询出目标浏览器版本,然后根据 @babel/compat-data 的数据库来过滤出这些浏览器版本里哪些特性不支持,之后引入对应的插件处理。12345/*使用 browserslist语法含义:ie 不低于 11 版本,全球超过 0.5% 使用,且还在维护更新的浏览器*/"targets": "ie >= 11, > 0.5%, not dead"
使用 useBuiltIns 配置完成 polyfill 注入,
设置useBuiltIns的值为usage时,我们不需要手动在入口文件引入polyfill,Babel将会根据我们的代码使用情况自动注入polyfill,如此一来在打包的时候将会相对地减少打包体积。唯一的问题:当项目中引入的第三方库有polyfill处理不当的情况下,将会出现引用异常的问题,使用社区广泛使用的流行库能降低这个风险。
上述两种方式已经达到了语句降级与 polyfill 的注入,即可以全量导入也可以按需导入,但也存在一些问题:因为是在全局环境注入 polyfill,那么开发第三方工具库时,就会存在对全局空间造成污染的问题;工具函数(例如asyncGeneratorStep、_asyncToGenerator)会在每个文件都生成,导致代码冗余。
- transform-runtime解决方案
使用 transform-runtime 可以很好地解决上面提到的问题。
plugin-transform-runtime可以主要做了三件事:
● 当开发者使用异步或生成器的时候,自动引入@babel/runtime/regenerator,开发者不必在入口文件做额外引入。
● 提供沙盒环境,避免全局环境的污染。
● 移除babel内联的helpers,统一使用@babel/runtime/helpers代替,减小打包体积。
@babel/preset-env和plugin-transform-runtime二者都可以设置使用corejs来处理polyfill,二者各有使用场景,在项目开发和类库开发的时候可以使用不同的配置。
不要同时为二者配置core-js的功能,以免产生复杂的不良后果。
useBuiltIns,其默认值是false,在不主动import的情况下不使用preset-env来实现polyfills,只使用其默认的语法转换功能。
先安装一下以下的包:12@babel/plugin-transform-runtime:编译时工具,用来转换语法和注入 polyfill@babel/runtime-corejs3:封装了基础库 core-js 和 regenerator-runtime
|
|
参考文章:Babel 7: @babel/preset-env & plugin-transform-runtime 小知识
ES 规范
前端ES规范是指ECMAScript规范。ECMAScript是ECMA(欧洲计算机制造商协会)制定的标准化脚本语言,其最为著名的实现是JavaScript。
学习ECMAScript的原因:
标准化:ECMAScript是JavaScript的规范,学习它可以让我们更好地理解JavaScript的底层原理和设计思想。
新特性:随着ECMAScript版本的更新,新的特性被不断引入,如ES6、ES7、ES8等,这些新特性可以提高我们的编程效率和代码质量。
大型应用:ECMAScript的终极目标是为了使JavaScript语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
常用版本:
ES5:这是ECMAScript的第五个版本,发布于2009年。它引入了严格模式、JSON支持、Object.create()等方法。
ES6(也称为ECMAScript 2015):这是目前前端开发中最常用的版本之一。它引入了许多新特性,如let和const关键字、箭头函数、模板字符串、解构赋值、Promise等。
兼容性:
ES5:在主流浏览器中都有很好的兼容性,包括IE9+、Chrome、Firefox、Safari等。
ES6:虽然ES6引入了许多新特性,但在旧版浏览器中可能存在兼容性问题。然而,随着现代前端工具(如Babel)的普及,我们可以将ES6代码转换为ES5代码,以确保在旧版浏览器中的兼容性。
总结来说,学习ECMAScript规范可以帮助我们更好地理解JavaScript的底层原理和设计思想,提高编程效率和代码质量。同时,随着版本的更新,新的特性被不断引入,可以满足更复杂的应用场景。在兼容性方面,虽然新版本的ECMAScript在某些旧版浏览器中可能存在兼容性问题,但我们可以使用前端工具进行转换以确保兼容性。
以下是对ECMAScript ES5到ES13(截至当前时间,即2024年)的一些主要特性和改进的概述:
新版本的ECMAScript在旧版浏览器中可能存在兼容性问题。然而,现代前端工具(如Babel)可以将较新的ECMAScript代码转换为更兼容的ES5代码,以确保在旧版浏览器中的兼容性。同时,随着浏览器的不断更新,对新版本ECMAScript的支持也在不断提高。
Deno
Deno 是一个跨平台的运行时,即基于 Google V8 引擎的运行时环境,该运行时环境是使用 Rust 语言开发的,并使用 Tokio 库来构建事件循环系统。
默认安全设置。除非 显式开启,否则不能访问文件、网络,也不能访问运行环境。
天生支持 TypeScript。
只有一个唯一的可执行文件。
自带实用工具,例如依赖检查器 (deno info) 和代码格式化工具 (deno fmt)。
有一套经过审核(审计)的标准模块, 确保与 Deno 兼容: deno.land/std
Deno与Node.js在多个方面存在显著的区别,以下是对两者在版本、区别及应用方面的详细比较:
版本
Node.js:自2009年发布以来,Node.js已经历了多个稳定版本的迭代,目前最新的稳定版本是根据发布时间而定的。
Deno:Deno是在2018年由Node.js的原始创建者Ryan Dahl创建的,旨在解决Node.js中存在的一些问题。目前,Deno也已经有了多个版本,最新的稳定版本也是根据发布时间而定的。
区别
安全性:
Node.js:默认情况下,Node.js对文件系统、网络等具有访问权限,需要开发者自行处理安全问题。
Deno:Deno默认在沙箱中执行代码,对文件系统、网络等的访问需要明确授权。这种默认安全设置使得Deno更加安全。
包管理:
Node.js:主要使用npm(Node Package Manager)进行包管理,具有庞大的生态系统。
Deno:不依赖于npm,而是自带一个包管理器,可以直接从URL导入模块。这使得Deno的包管理更加灵活和简洁。
语言支持:
Node.js:主要支持JavaScript。
Deno:除了支持JavaScript外,还默认支持TypeScript,为开发者提供了更好的类型检查和开发体验。
实现语言:
Node.js:使用C++编写。
Deno:核心部分使用Rust编写,这使得Deno在性能和安全方面具有一定的优势。
生态系统:
Node.js:拥有庞大的生态系统和社区支持,具有许多成熟的开源模块和工具。
Deno:虽然生态系统相对较小,但正在不断发展壮大,特别是在TypeScript支持和安全性方面具有一定的优势。
应用
Node.js:由于其庞大的生态系统和社区支持,Node.js适用于各种类型的应用程序开发,如Web应用程序、API服务器、实时通信、物联网等。
Deno:由于其默认的安全设置和TypeScript支持,Deno特别适用于需要高安全性和良好开发体验的场景,如前端开发、后端API服务、数据处理等。此外,Deno也适合用于构建跨平台的应用程序和游戏。
综上所述,Deno与Node.js在安全性、包管理、语言支持、实现语言和生态系统等方面存在显著的区别。在选择使用哪个平台时,需要根据项目的具体需求和个人偏好进行权衡。
使用 deno run <文件名>
运行文件。
关于Deno的权限,首先我们需要明确的是,Deno具有默认的安全设计。当你在使用Deno进行文件访问、网络请求或环境变量获取时,你需要明确地开启相关的权限。
Deno的权限管理主要通过命令行选项来实现,以下是一些常用的权限选项:
环境权限:Deno默认不允许程序访问环境变量。如果你需要访问环境变量,你需要使用–allow-env选项。
网络权限:与Node.js不同,Deno默认不允许程序进行网络请求。如果你需要访问网络资源,你需要使用–allow-net选项,并可能指定具体的域名或IP地址。
文件系统读写权限:Deno也默认不允许程序读写文件系统。如果你需要读取或写入文件,你需要使用–allow-read和–allow-write选项,并指定具体的文件或目录路径。
运行子进程权限:如果Deno程序需要运行子进程(如执行系统命令),你需要使用–allow-run选项。
这些权限选项可以在deno run命令中直接使用,例如:deno run --allow-net --allow-read my_script.ts
,这条命令会运行my_script.ts文件,并允许它进行网络请求和文件系统读取操作。
需要注意的是,虽然Deno的默认安全设计可能会增加一些复杂性,但它也确保了程序在运行时具有更高的安全性。通过显式地声明所需的权限,你可以更好地控制你的程序能够做什么,从而降低了潜在的安全风险。
Deno 没有包管理工具,所以不需要创建 package.json 文件。Deno 提供了通过 URL 引入第三方包的形式。第三方库都以 “https://deno.land/x” 开头,后面跟第三方库标识和入口文件。
Oak是一个为Deno内置HTTP服务器和Node.js(16.5及以上版本)设计的中间件框架。它深受Koa框架的影响,为熟悉Express和Koa以及有一定Deno知识的开发者提供了一种无缝且高效的Web开发体验。
Oak的主要特点包括:
设计简洁,易于上手:其核心是Application类,负责管理服务器、运行中间件并处理请求中的错误。
中间件和路由器:通过.use()方法添加中间件,.listen()方法启动服务器并开始处理请求。Oak还提供了一个基于Deno的中间件路由器,简化了路由管理和处理。
技术剖析:Oak的工作原理基于中间件栈的概念,每个中间件函数接收一个上下文对象(ctx)和指向下一个中间件的引用(next)。上下文包含了请求和响应的相关信息,可以控制请求处理流程。
前端工程化核心与构建工具实战
从前端的开发流程来看,工程化主要包括技术选型、统一规范、测试、部署、监控、性能优化、重构和文档。
- 代码规范
在开发过程中,代码规范一直占较大的权重,因为一致的代码规范能促使团队更好地协作,降低代码维护的成本,更好地促进项目成员的成长,可以更容易地构建和编译代码,并对代码进行检视和重构。
在前端社区中,较为成熟的规范如下:
Airbnb规范, GitHub上的star数量为143k+,足见它的受欢迎程度。这套规范不仅包含了
JavaScript的,还包含了React、CSS、Ruby、Css-in-JavaScript和Sass的,非常方便。
standard规范: GitHub上的star数量为28.9k+。这个规范是通过npm包的形式安装的,然后在package.json文件的scripts中添加如下命令进行检查。 - 脚手架框架:commander
- 自动化部署
一般公司使用的Jenkins 持续集成部署。
deploy-cli-service :前端一键自动化部署脚手架服务,支持开发、测试、生产多环境配置。配置好后一键即可自动完成部署。如果不想把服务器密码保存在配置文件中,也可以在配置文件中删除 password 字段。在部署的时候会弹出输入密码界面。要注意信息安全 - 配置负载均衡 Nginx
Nginx是一种轻量级、高性能、低内存的Web服务器和反向代理服务器。传统的Web服务器对
于客户端的每一个连接都会创建一个新的进程或线程来处理,也创建了新的运行时,会消耗额外的
内存。随着连接的增多,Web服务器的响应速度开始变慢,延退增加。Nginx对传统服务而言,既
优化了服务器资源的使用,也支持连接的动态增加。
Nginx有如下几个比较显著的优点
● 热部署:因为Nginx的master进程和worker进程是独立设计的(通常是一个master进程
和多个worker进程),所以可以在不间断服务的前提下升级可执行文件和配置文件等。
● 高并发:经官方测试,Nginx支持的连接数能够支摔5万并发连接数,在实际生产环境中
可以支撑2万~4万并发连接数。如果是clustered,则支持的连接数会更高。
● 内存消耗少。
Nginx常用命令:1234567nginx #启动Nginxnginx -s quit #快速停止Nginxnginx -V #查看版本及配置文件地址nginx -V #查看版本nginx -s reload | reopen | stop | quit #重新加载配置、重启、停止、安全关闭nginx -h #帮助nginx -t #校验配置文件
在前端开发中经常需要配置反向代理,以解决跨域问题。
gzip是一种常用的网页压缩技术,网页经过gzip压缩之后,体积通常变为原来的一半甚至更小。
更小的网页体积意味着带宽的节约和传输速度的提升。使用gzip压缩不仅需要配置Nginx,浏览器
端也需要配合,需要在请求消息头中包含Accept-Encoding:gzip。
Nginx在拿到这个请求后,如果有相应配置,就返回经gzip压缩过的文件给浏览器,并在回复
时加上content-encoding:gzip,以告诉浏览器自己采用的压缩方式。
通过Nginx配置负载均衡。负载均衡的主要思想就是把负载按照权重配置合理地分发到多
个服务器上,实现压力分流,降低服务器岩机的概率。
图片防盗配置:由于图片链接可以跨域访问,所以当图片被其他网站引用时,无形中就增加了服务器的负担。
Jest 测试核心
在前端领域,目前主流测试框架有Jest、Mocha和Jasmine。Jest是Facebook开源的一个前端测试框架,它集成了expect、chalk、jsdom、jasmine、sinon等测试库,可用于React、Angular、Vue、Node和TypeScript的单元测试。
https://jestjs.io/zh-Hans/docs/getting-started
打包工具
● Webpack:功能强大,支持模块化处理,插件丰富,适合大型项目。
● Vite:快速冷启动,按需编译,简单配置,适合追求开发效率的项目。
● Parcel、Rollup:Parcel易于上手,Rollup适合库的打包。
● Vite与Webpack的HMR对比:Webpack:重新构建整个模块,时间较长。Vite:只更新改变的文件,速度更快。
前端核心模块的设计与实现
常用设计模式
1、单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。其实就是实例只会被创建一次,后续都是复用这一个实例。
2、策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
策略模式至少要由两部分组成:
封装了各类算法的策略类
接受参数的环境类,然后把请求委托给策略类进行处理
3、代理模式
访问对象时为一个对象提供一个代用品或者占位符,以便控制对它的访问。
4、迭代器模式
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
5、发布订阅模式
又称观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知。订阅者一旦订阅后,发布者发布事件都会通知到 每个订阅者,但是如果过度使用会导致难以跟踪维护,特别是多个发布者和订阅者嵌套的时候。
6、命令模式
指一个执行某些特定事情的命令。命令模式的特点就是不知道请求的接收者是谁,也不知道被请求的操作是什么,所以需要借助命令对象来执行传入的命令。
7、模板方法模式
模板方法是一种只需要使用继承就可以实现的非常简单的模式。这是一种典型通过封装来提高扩展性的设计模式。
8、享元模式
运用共享技术来有效支持大量细粒度的对象。对象池工厂 也就是享元模式。
9、职责链模式
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的偶合关系,将这些对象练成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
10、装饰者模式
可以动态的给某个对象添加一些额外的职责,而不会影响从这个类派生的其他对象。
11、状态模式
区分事物内部的状态,事物内部状态的改变往往会带来事物的行为改变。状态模式定义了状态和行为之间的关系, 缺点是会在系统中定义许多状态类。
12、适配器模式
解决两个软件实体间的接口不兼容。主要就是解决两个已有接口之间不匹配的问题。
参考:JavaScript常用设计模式与实践 - 知乎
V8 引擎
V8引擎可以运行在多个主流平台上,它既可以单独运行,也可以嵌入其他C++程序中。V8引擎是WebKit的子集。
Safari使用的是JavaScriptCore,它随着WebKit一起发布。V8引擎是由Google公司开发的,实现了ES的ECMA-262规范。
JavaScript引擎是能够将JavaScript代码处理并执行的运行环境,不要把它跟浏览器内核搞混了!内核(如Webkit、Trident、Gecko等)主要是将页面转变成可视化的图像结果,即渲染出来,所以通常也被称为渲染引擎。
渲染引擎和JavaScript引擎是浏览器处理网页过程的两个核心部分,它们是相互合作的关系。
一般来说,渲染引擎根据JavaScript提供的桥接接口提供给JavaScript访问DOM的能力,很多HTML5也是通过桥接接口实现的。
桥接接口复杂而不高效往往是一个性能瓶颈,所以才说DOM的频繁使用会导致性能问题。
js的数组本质是个对象,但在v8中,以非负整数为key的元素被称为Element,每个对象都有一个指向Element数组的指针,其存放和其他属性是分开的,这其实也是针对数组的优化。
为了掌控大而稀疏的数组,V8内部有两种数组存储方式:
快速元素:对于紧凑型关键字集合,进行线性存储;
字典元素:对于其它情况,使用哈希表。
最好别导致数组存储方式在两者之间切换。
开发指引:
● 使用非负整数作为数组下标,不要使用负数、浮点数、字符串等会被认为是一般性的Object的key。
● 在数组中存储同一类型的元素。
● 尽量的避免使用不连续的索引值,而且从0开始。
● 如果非要存不同类型的元素,那么使用字面直接量初始化而不是一个一个的存入。
● 预先分配数组大小,这在大多数状况下都有较大的性能提升,可以忽略掉64K的限制,但是小于万量级的话差别几可无视。
● 不要逆向赋值!!不要逆向赋值!!不要逆向赋值!!
● 最好不要随便删除数组元素,这可能会导致转为Dictionary Elements,据说洞变少可能会被v8优化回紧凑结构,但是这是不可依赖的行为。
● 先赋值,再访问,避免使用 arr[100] == null 或者隐式转换的判断性访问。
对象(隐藏类):v8利用动态创建隐藏内部类的方式动态地将属性的内存地址记录在对象内,从而提升整体的属性访问速度。避免了通过字符串匹配的方式来查找属性值。隐藏类是为Object服务的,相同结构的Object会共享隐藏类,当结构发生了改变,对应的隐藏类也会发生改变,要么复用,要么新增。而且会将使用过的隐藏类结构通过内嵌缓存(inline cache)缓存起来,以便复用时可以快速的访问偏移值。
开发指引(编写对V8友好的高性能代码):
● 一次性的初始化所有的属性,而不是后续的动态增加。
● 属性初始化的顺序应当一致,以便保证能够复用隐藏类。
● delete 会触发隐藏类的改变,如果是为了内存回收,设置为null是更好的选择。
在JavaScript中有6种基础类型,分别是String、Number、Boolean、Null、Undefined和Symbol。这些类型的值都有固定的存储大小,往往保存在栈中,由系统自动分配存储空间,我们可以直接访问。其他类型为引用类型,比如对象,它在内存中分配的值是不固定的。引用类型的变量值是保存在堆内存中的(堆是非结构化区域,堆中的对象占用分配的内存。这种分配是动态的,因为对象的寿命和数量未知,所以需要在运行时分配和释放),不可以直接访问。
JavaScript 具有垃圾自动回收机制(Garbage Collection)简称 GC。垃圾回收机制会中断整个代码执行,释放不可能再被使用的变量,释放内存,这个工作机制是周期性的。
标记清除:标记清除是目前大部分 JavaScript 引擎使用的判断方式,通过标记变量的状态来确定是否可被回收。当变量在环境中被声明时标记进入环境,理论上永远不要释放进入环境的变量,因为它可以在环境中的任何位置、任何时刻被访问。当环境被销毁(如函数执行完),则变量被标记离开环境等待回收。
引用计数:JavaScript 引擎维护一张引用表,保存内存中所有的资源的引用次数。资源被引用一次则引用 +1,资源被去掉引用或者退出变量的函数作用域时,则引用 -1,当资源的引用次数为0时,说明无法访问这个值,则等待回收。无法解决循环引用,无法回收(致命问题)。
(注:引用计数从 1 到 0 这个过程可能不执行,而是直接标记可被回收,不再进行加减运算节约开销)
在 V8 引擎新版本中引入了两种优化方法:1. 分代回收(Generation GC),2. 增量 GC(increment GC)。
分代回收:目的是通过对象的使用频率、存在时长区分新生代与老生代对象。多回收新生代区(young generation),少回收老生代区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。
增量 GC:把需要长耗时的遍历、回收操作拆分运行,减少中断时间,但是会增大上下文切换开销。
闭包会使程序逻辑变复杂,有时会看不清楚是否对象内存被释放,因此要注意释放闭包中的大对象, 否则会引起内存泄露。
浏览器是单线程的,代码在执行栈(Call Stack)中执行,这意味着它在任何时候只能执行一个任务。单线程是有必要的,因为JavaScript最初最主要的执行环境是浏览器。JavaScript面对的是各种各样的操作(如操作DOM、操作CSS等)。如果是多线程执行,则很难在频繁操作的情况下保证一致性。即便保证了一致性,对性能也有较大的影响。
然而,这并不意味着浏览器无法处理多个任务。为了实现这一点,浏览器引入了事件循环(Event Loop)机制。浏览器中有很多操作是异步的,比如网络请求、定时器、用户交互等。浏览器异步处理完这些任务,将执行结果放到回调任务队列(Queue),事件循环机制会不断地检查任务队列,当执行栈空闲时,如果队列中有任务,就取出任务并执行,然后再检查下一个任务,如此循环往复,直到任务队列为空。
在浏览器事件循环中,任务被分为两种:宏任务(macrotask)和微任务(microtask),相应的有两个任务队列。
在浏览器端,会被推入宏任务和微任务队列中的方法的回调如下:
宏任务:IO、setTimeout、setlnterval和requestAnimationFrame。
微任务:Promise.then、catch、finally和await。
微任务的优先级高于宏任务。如果在执行宏任务的过程中产生了微任务,那么这些微任务会在当前宏任务结束后,下一个宏任务开始前被执行。这样可以保证更快的响应,因为微任务通常用于处理一些更紧急的、需要快速响应的操作。
宏任务和微任务是前端开发中处理异步操作的基础,浏览器的事件循环机制通过宏任务和微任务,实现了同步和异步操作的统一调度。
了解宏任务和微任务的执行顺序,可以帮助我们更好地理解和使用异步编程模式,并且更好地理解和预测你的代码行为。通过合理地使用宏任务和微任务,我们可以优化代码的性能和响应速度,提高应用程序的稳定性和可维护性。
使用数字的教训:尽量使用可以用31位有符号整数表示的数。
当类型可以改变时,V8使用标记来高效的标识其值。V8通过其值来推断你会以什么类型的数字来对待它。因为这些类型可以动态改变,所以一旦V8完成了推断,就会通过标记高效完成值的标识。不过有的时候改变类型标记还是比较消耗性能的,我们最好保持数字的类型始终不变。通常标识为有符号的31位整数是最优的。
参考:高性能的JavaScript开发
性能优化指南
图片优化和 DOM 优化建议:
第一,减少图片。第二,结果可以通过 CSS 实现。第三,使用合适的图片格式。
JavaScript代码优化建议:
- JavaScript文件加载
在浏览器渲染过程中,当浏览器遇到标签时,浏览器会停止处理页面,让出当前主线程,转去执行JavaScript代码,等JavaScript代码执行完成后再继续解析和渲染页面。同样的情况也发生在使用src属性加载JavaScript代码的过程中。浏览器必须先花时间下载外链文件中的代码,然后解析并执行它。在这个过程中,页面渲染和用户交互完全被阻塞了。所以推荐将所有标签都尽可能放在标签的底部,以尽量减少下载对页面渲染的影响。
每个浏览器都有最大连接数,所以尽量减少JavaScript文件的加载数量是一个比较常用的方法。但是这个方法的作用是有限的,原因有两个。第一,在大型项目中把JavaScript文件合并成一个bundle不是很现实;第二,合并后的bundle文件如果过大,则仍不能解决浏览器长时间无响应的问题。
script标签有两个扩展属性:defer(HTML4引人)和async(HTML5引入)。
defer:延迟加载脚本,在文档解析完成后开始执行,并且在DOMContentLoaded事件之前执行完成。
async:异步加载脚本,需下载完毕后再执行,在load事件之前执行完成。
总的来说,defer是“渲染完再执行”,async是“下载完就执行”。 - JavaScript 文件缓存
浏览器的缓存机制:
在 Header 内的字段用于控制缓存机制,老方法 Expires,记录的绝对值;新方法 Cache-Control 多了一堆选项,记录的时间是相对值。获取缓存:检测缓存是否过期,如果没过期取缓存,优先从内存,其次硬盘,如果过期,则与服务器协商缓存是否仍然可用,如果不可用则获取,可用取缓存。
Service Worker是由事件驱动的具有生命周期并且独立于浏览器的主线程。它可以拦截处理页面的所有网络请求(fetch),还可以访问缓存和IdexDB,支持推送,并且可以让开发者自己控制、管理缓存的内容及版本,为离线弱网环境下的Web运行提供了可能。 - JavaScript代码细节优化
减少回流(重排)和重绘:
想要避免回流与重绘,最直接的做法是避免可能会引发回流与重绘的DOM操作,具体如下。
● 避免操作DOM的几何属性。元素的几何属性通常包含height、width、margin、padding、left和border等。
● 避免改变DOM树的结构。这里涉及的操作主要是增加、修改和删除节点。
● 避免获得一些特殊的值。当我们用到client*(top,leftwidth,height)、offset、scroll等属性和getComputedStyle等方法时,也会触发回流。因为这些属性是通过即时计算得到的。
在某些场景下,回调方法会反复执行多次,比如窗口的resize时间、滚动条的scroll事件,以及键盘的keydown、keyup事件和鼠标的mouseover、mousemove事件等,这些反复执行的结果会引发大量的计算,从而导致页面卡顿,这不是我们想要的结果。为了应对这种场景,节流(throttle:当持续触发事件时,应保证在一定时间段内只调用一次事件处理函数)和防抖(debounce:当持续触发事件时,如果在一定时间段内没有触发事件,那么事件处理函数会再执行一次。如果在设定的时间到来之前,又一次触发了事件,则重新开始延时)就诞生了。WebAssembly(简称 wasm )
WebAssembly | MDN
关于 WebAssembly 的文档可以参考 https://webassembly.org/
WebAssembly 只是提供了在浏览器上 (or node.js) 运行非 JavaScript 编程语言的能力。实际上 wasm 是体积小且加载快的二进制格式, 其目标就是充分发挥硬件能力以达到原生执行效率。
WebAssembly 是为下列目标而生的:
● 快速、高效、可移植——通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。
● 可读、可调试——WebAssembly 是一门低阶语言,但是它有确实有一种人类可读的文本格式(其标准即将得到最终版本),这允许通过手工来写代码,看代码以及调试代码。
● 保持安全——WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
● 不破坏 Web——WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容。
使用 Emscripten 移植一个 C/C++ 应用程序。
将 golang 打包为 WASM:通常有两种打包方式,一种是 golang 自带的,另外是使用 tinygo。推荐使用 tinygo,因为编译出的 wasm 模块更小。
Golang 在1.11版本中引入了 WebAssembly 支持,设置 golang 交叉编译参数,目标系统 GOOS=js
和目标架构 GOARCH=wasm
以编译WebAssembly。
小程序WebAssembly:性能与体验 / WXWebAssembly。
- 应用前景
wasm 的优点在于一个字——快!但这仅限于 wasm 的沙箱之内,而 wasm 与 js 的交互相当耗时,所以在使用的时候应当注意:尽可能将纯计算逻辑限定在 wasm 内部,应该尽量减少 js 与 wasm 的来回调用(所以业务代码不适合也不应该编译为 wasm)
概括的说,wasm 的应用场景有以下两个方面:
- 复杂的计算可以使用 wasm 来提高性能
比如: 视频/音乐编辑、游戏引擎、AutoCAD、Figma。 - 把一些 C++ / Rust/ go 写的 native 库移植到浏览器里来增强浏览器的能力
这是目前最适合使用 wasm 的场景。