注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

什么,项目构建时内存溢出了?了解一下 node 内存限制

背景在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出的问题。当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。今天听同事分享了一个新方法,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。正文但 Node 进程...
继续阅读 »

背景

在之前的一篇文章中, 我们遇到了一个项目在构建时内存溢出的问题。

当时的解决方案是: 直接调大 node 的内存限制,避免达到内存上限。

今天听同事分享了一个新方法,觉得不错, 特此记录, 顺便分享给大家, 希望对大家有所帮助。

正文


但 Node 进程的内存限制会是多少呢?

在网上查阅了到如下描述:

Currently, by default V8 has a memory limit of 512mb on 32-bit systems, and 1gb on 64-bit systems. The limit can be raised by setting --max-old-space-size to a maximum of ~1gb (32-bit) and ~1.7gb (64-bit), but it is recommended that you split your single process into several workers if you are hitting memory limits.

翻译一下:

当前,默认情况下,V8在32位系统上的内存限制为512mb,在64位系统上的内存限制为1gb。

可以通过将--max-old-space-size设置为最大〜1gb(32位)和〜1.7gb(64位)来提高此限制,但是如果达到内存限制, 建议您将单个进程拆分为多个工作进程

如果你想知道自己电脑的内存限制有多大, 可以直接把内存撑爆, 看报错。

运行如下代码:

// Small program to test the maximum amount of allocations in multiple blocks.
// This script searches for the largest allocation amount.

// Allocate a certain size to test if it can be done.
function alloc (size) {
const numbers = size / 8;
const arr = []
arr.length = numbers; // Simulate allocation of 'size' bytes.
for (let i = 0; i < numbers; i++) {
arr[i] = i;
}
return arr;
};

// Keep allocations referenced so they aren't garbage collected.
const allocations = [];

// Allocate successively larger sizes, doubling each time until we hit the limit.
function allocToMax () {
console.log("Start");

const field = 'heapUsed';
const mu = process.memoryUsage();

console.log(mu);

const gbStart = mu[field] / 1024 / 1024 / 1024;

console.log(`Start ${Math.round(gbStart * 100) / 100} GB`);

let allocationStep = 100 * 1024;

// Infinite loop
while (true) {
// Allocate memory.
const allocation = alloc(allocationStep);
// Allocate and keep a reference so the allocated memory isn't garbage collected.
allocations.push(allocation);
// Check how much memory is now allocated.
const mu = process.memoryUsage();
const gbNow = mu[field] / 1024 / 1024 / 1024;

console.log(`Allocated since start ${Math.round((gbNow - gbStart) * 100) / 100} GB`);
}

// Infinite loop, never get here.
};

allocToMax();


我的电脑是 Macbook Pro masOS Catalina 16GB,Node 版本是 v12.16.1,这段代码大概在 1.6 GB 左右内存时候抛出异常。

那我们现在知道 Node Process 确实是有一个内存限制的, 那我们就来增大它的内存限制再试一下。

用 node --max-old-space-size=6000 来运行这段代码,得到如下结果:


内存达到 4.6G 的时候也溢出了。

你可能会问, node 不是有内存回收吗?这个我们在下面会讲。

使用这个参数:node --max-old-space-size=6000, 我们增加的内存中老生代区域的大小,比较暴力。

就像上文中提到的: 如果达到内存限制, 建议您将单个进程拆分为多个工作进程

这个项目是一个 ts 项目,ts 文件的编译是比较占用内存的,如果把这部分独立成一个单独的进程, 情况也会有所改善。

因为 ts-loader 内部调用了 tsc,在使用 ts-loader 时,会使用 tsconfig.js配置文件。

当项目中的代码变的越来越多,体积也越来越庞大时,项目编译时间也随之增加。

这是因为 Typescript 的语义检查器必须在每次重建时检查所有文件

ts-loader 提供了一个 transpileOnly 选项,它默认为 false,我们可以把它设置为 true,这样项目编译时就不会进行类型检查,也不会输出声明文件。

对一下 transpileOnly 分别设置 false 和 true 的项目构建速度对比:

  • 当 transpileOnly 为 false 时,整体构建时间为 4.88s.
  • 当 transpileOnly 为 true 时,整体构建时间为 2.40s.

虽然构建速度提升了,但是有了一个弊端: 打包编译不会进行类型检查

好在官方推荐了这样一个插件, 提供了这样的能力: fork-ts-checker-webpack-plugin

官方示例的使用也非常简单:

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

module.exports = {
...
plugins: [
new ForkTsCheckerWebpackPlugin()
]
}

在我这个实际的项目中,vue.config.js 修改如下:

configureWebpack: config => {
// get a reference to the existing ForkTsCheckerWebpackPlugin
const existingForkTsChecker = config.plugins.filter(
p => p instanceof ForkTsCheckerWebpackPlugin,
)[0];

// remove the existing ForkTsCheckerWebpackPlugin
// so that we can replace it with our modified version
config.plugins = config.plugins.filter(
p => !(p instanceof ForkTsCheckerWebpackPlugin),
);

// copy the options from the original ForkTsCheckerWebpackPlugin
// instance and add the memoryLimit property
const forkTsCheckerOptions = existingForkTsChecker.options;

forkTsCheckerOptions.memoryLimit = 4096;

config.plugins.push(new ForkTsCheckerWebpackPlugin(forkTsCheckerOptions));
}

修改之后, 构建就成功了。

关于垃圾回收

在 Node.js 里面,V8 自动帮助我们进行垃圾回收, 让我们简单看一下V8中如何处理内存。

一些定义

  • 常驻集大小:是RAM中保存的进程所占用的内存部分,其中包括:

    1. 代码本身
  • stack:包含原始类型和对对象的引用
  • 堆:存储引用类型,例如对象,字符串或闭包
  • 对象的浅层大小:对象本身持有的内存大小
  • 对象的保留大小:删除对象及其相关对象后释放的内存大小

垃圾收集器如何工作

垃圾回收是回收由应用程序不再使用的对象所占用的内存的过程。

通常,内存分配很便宜,而内存池用完时收集起来很昂贵。

如果无法从根节点访问对象,则该对象是垃圾回收的候选对象,因此该对象不会被根对象或任何其他活动对象引用。

根对象可以是全局对象,DOM元素或局部变量。

堆有两个主要部分,即 New Space和 Old Space

新空间是进行新分配的地方。

在这里收集垃圾的速度很快,大小约为1-8MB

留存在新空间中的物体被称为新生代

在新空间中幸存下来的物体被提升的旧空间-它们被称为老生代

旧空间中的分配速度很快,但是收集费用很高,因此很少执行。

node 垃圾回收

Why is garbage collection expensive?

The V8 JavaScript engine employs a stop-the-world garbage collector mechanism.

In practice, it means that the program stops execution while garbage collection is in progress.

通常,约20%的年轻一代可以存活到老一代,旧空间的收集工作将在耗尽后才开始。

为此,V8 引擎使用两种不同的收集算法

  1. Scavenge: 速度很快,可在新生代上运行,
  2. Mark-Sweep: 速度较慢,并且可以在老生代上运行。

篇幅有限,关于v8垃圾回收的更多信息,可以参考如下文章:

  1. http://jayconrod.com/posts/55...
  2. https://juejin.cn/post/684490...
  3. https://juejin.cn/post/684490...

总结

小小总结一下,上文介绍了两种方式:

  1. 直接加大内存,使用: node --max-old-space-size=4096
  2. 把一些耗内存进程独立出去, 使用了一个插件: fork-ts-checker-webpack-plugin

希望大家留个印象, 记得这两种方式。

好了, 内容就这么多, 谢谢。

才疏学浅,如有错误, 欢迎指正。

谢谢。

原文:https://segmentfault.com/a/1190000039877970


收起阅读 »

前端常用图片文件下载上传方法

本文整理了前端常用的下载文件以及上传文件的方法例子均以vue+element ui+axios为例,不使用el封装好的上传组件,这里自行进行封装实现先附上demo上传文件以图片为例,文件上传可以省略预览图片功能图片上传可以使用2种方式:文件流和base64;1...
继续阅读 »

本文整理了前端常用的下载文件以及上传文件的方法
例子均以vue+element ui+axios为例,不使用el封装好的上传组件,这里自行进行封装实现

先附上demo

上传文件

以图片为例,文件上传可以省略预览图片功能

图片上传可以使用2种方式:文件流base64;

1.文件流上传+预览

<input type="file" id='imgBlob' @change='changeImgBlob' />
<el-image style="width: 100px; height: 100px" :src="imgBlobSrc"></el-image>
// data
imgBlobSrc: ""

// methods
changeImgBlob() {
let file = document.querySelector("#imgBlob");
/**
*图片预览
*更适合PC端,兼容ie7+,主要功能点是window.URL.createObjectURL
*/
var ua = navigator.userAgent.toLowerCase();
if (/msie/.test(ua)) {
this.imgBlobSrc = file.value;
} else {
this.imgBlobSrc = window.URL.createObjectURL(file.files[0]);
}
//上传后台
const fd = new FormData();
fd.append("files", file.files[0]);
fd.append("xxxx", 11111); //其他字段,根据实际情况来
axios({
url: "/yoorUrl", //URL,根据实际情况来
method: "post",
headers: { "Content-Type": "multipart/form-data" },
data: fd
});
}



2.Base64上传+预览

<input type="file" id='imgBase' @change='changeImgBase' />
<el-image style="width: 100px; height: 100px" :src="imgBaseSrc"></el-image>
// data
imgBaseSrc : ""

// methods
changeImgBase() {
let that = this;
let file = document.querySelector("#imgBase");
/**
*图片预览
*更适合H5页面,兼容ie10+,图片base64显示,主要功能点是FileReader和readAsDataURL
*/
if (window.FileReader) {
var fr = new FileReader();
fr.onloadend = function (e) {
that.imgBaseSrc = e.target.result;
//上传后台
axios({
url: "/yoorUrl", //URL,根据实际情况来
method: "post",
data: {
files: that.imgBaseSrc
}
});
};
fr.readAsDataURL(file.files[0]);
}
}


下载文件

图片下载

假设需要下载图片为url文件流处理和这个一样

<el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
<el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
  • 注意:这里需要指定 responseTypeblob
//data
downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
//methods
downloadImg() {
axios({
url: this.downloadImgSrc, //URL,根据实际情况来
method: "get",
responseType: "blob"
}).then(function (response) {
const link = document.createElement("a");
let blob = new Blob([response.data], { type: response.data.type });
let url = URL.createObjectURL(blob);
link.href = url;
link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
link.click();
document.body.removeChild(link);
});
}

文件下载(以pdf为例)

<el-image style="width: 100px; height: 100px" :src="downloadImgSrc"></el-image>
<el-button type="warning" round plain size="mini" @click='downloadImg'>点击下载</el-button>
//data
downloadImgSrc:'https://i.picsum.photos/id/452/400/300.jpg?hmac=0-o_NOka_K6sQ_sUD84nxkExoDk3Bc0Qi7Y541CQZEs'
//methods
downloadImg() {
axios({
url: this.downloadImgSrc, //URL,根据实际情况来
method: "get",
responseType: "blob"
}).then(function (response) {
const link = document.createElement("a");
let blob = new Blob([response.data], { type: response.data.type });
let url = URL.createObjectURL(blob);
link.href = url;
link.download = `实际需要的文件名.${response.data.type.split('/')[1]}`;
link.click();
document.body.removeChild(link);
});
}

pdf预览可以参考如何预览以及下载pdf文件

原文:https://segmentfault.com/a/1190000039893814



收起阅读 »

web 埋点实现原理了解一下

前言埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情...
继续阅读 »

前言

埋点,是网站分析的一种常用的数据采集方法。我们主要用来采集用户行为数据(例如页面访问路径,点击了什么元素)进行数据分析,从而让运营同学更加合理的安排运营计划。现在市面上有很多第三方埋点服务商,百度统计,友盟,growingIO 等大家应该都不太陌生,大多情况下大家都只是使用,最近我研究了下 web 埋点,你要不要了解下。

现有埋点三大类型

用户行为分析是一个大系统,一个典型的数据平台。由用户数据采集,用户行为建模分析,可视化报表展示几个模块构成。现有的埋点采集方案可以大致被分为三种,手动埋点,可视化埋点,无埋点
  1. 手动埋点
    手动代码埋点比较常见,需要调用埋点的业务方在需要采集数据的地方调用埋点的方法。优点是流量可控,业务方可以根据需要在任意地点任意场景进行数据采集,采集信息也完全由业务方来控制。这样的有点也带来了一些弊端,需要业务方来写死方法,如果采集方案变了,业务方也需要重新修改代码,重新发布。
  2. 可视化埋点
    可是化埋点是近今年的埋点趋势,很多大厂自己的数据埋点部门也都开始做这块。优点是业务方工作量少,缺点则是技术上推广和实现起来有点难(业务方前端代码规范是个大前提)。阿里的活动页很多都是运营通过可视化的界面拖拽配置实现,这些活动控件元素都带有唯一标识。通过埋点配置后台,将元素与要采集事件关联起来,可以自动生成埋点代码嵌入到页面中。
  3. 无埋点
    无埋点则是前端自动采集全部事件,上报埋点数据,由后端来过滤和计算出有用的数据,优点是前端只要加载埋点脚本。缺点是流量和采集的数据过于庞大,服务器性能压力山大,主流的 GrowingIO 就是这种实现方案。

我们暂时放弃可视化埋点的实现,在 手动埋点 和 无埋点 上进行了尝试,为了便于描述,下文我会称采集脚本为 SDK。

思考几个问题

埋点开发需要考虑很多内容,贯穿着不轻易动手写代码的原则,我们在开发前先思考下面这几个问题
  1. 我们要采集什么内容,进行哪些采集接口的约定
  2. 业务方通过什么方式来调用我们的采集脚本
  3. 手动埋点:SDK 需要封装一个方法给业务方进行调用,传参方式业务方可控
  4. 无埋点:考虑到数据量对于服务器的压力,我们需要对无埋点进行开关配置,可以配置进行哪些元素进行无埋点采集
  5. 用户标识:游客用户和登录用户的采集数据怎么进行区分关联
  6. 设备Id:用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样,怎么实现
  7. 单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异
  8. 混合应用:app 与 h5 的混合应用我们要怎么进行通讯

我们要采集什么内容,进行哪些采集接口的约定

第一期我们先实现对 PV(即页面浏览量或点击量) 、UV(一天内同个访客多次访问) 、点击量、用户的访问路径的基础指标的采集。精细化分析的流量转化需要和业务相关,需要和数据分析方做约定,我们预留扩展。所以我们的采集接口需要进行以下的约定

{
"header":{ // HTTP 头部
"X-Device-Id":" 550e8400-e29b-41d4-a716-446655440000", //设备ID,用来区分用户设备
"X-Source-Url":"https://www.baidu.com/", //源地址,关联用户的整个操作流程,用于用户行为路径分析,例如登录,到首页,进入商品详情,退出这一整个完整的路径
"X-Current-Url":"", //当前地址,用户行为发生的页面
"X-User-Id":"",//用户ID,统计登录用户行为
},
"body":[{ // HTTP Body体
"PageSessionID":"", //页面标识ID,用来区分页面事件,例如加载和离开我们会发两个事件,这个标识可以让我们知道这个事件是发生在一个页面上
"Event":"loaded", //事件类型,区分用户行为事件
"PageTitle": "埋点测试页", //页面标题,直观看到用户访问页面
"CurrentTime": “1517798922201”, //事件发生的时间
"ExtraInfo": {
} //扩展字段,对具体业务分析的传参
}]
}

以上就是我们现在约定好了的通用的事件采集的接口,所传的参数基本上会根据采集事件的不同而发生变化。但是在用户的整一个访问行为中,用户的设备是不会变化的,如果你想采集设备信息可以重新约定一个接口,在整个采集开始之前发送设备信息,这样可以避免在事件采集接口上重复采集固定数据。

{
"header":{ // HTTP 头部
"X-Device-Id" :"550e8400-e29b-41d4-a716-446655440000" , // 设备id
},
"body":{ // HTTP Body体
"DeviceType": "web" , //设备类型
"ScreenWide" : 768 , // 屏幕宽
"ScreenHigh": 1366 , // 屏幕高
"Language": "zh-cn" //语言
}
}

手动埋点:SDK

如果业务方需要采集更多业务定制的数据,可以调用我们暴露出的方法进行采集

//自定义事件
sdk.dispatch('customEvent',{extraInfo:'自定义事件的额外信息'})

游客与用户关联

我们使用 userId 来做用户标识,同一个设备的用户,从游客用户切换到登录用户,如果我们要把他们关联起来,需要有一个设备Id 做关联

web 设备Id

用户通过浏览器来访问 web 页面,设备Id需要存储在浏览器上,同一个用户访问不同的业务方网站,设备Id要保持一样。web 变量存储,我们第一时间想到的就是 cookie,sessionStorage,localStorage,但是这3种存储方式都和访问资源的域名相关。我们总不能每次访问一个网站就新建一个设备指纹吧,所以我们需要通过一个方法来跨域共享设备指纹

我们想到的方案是,通过嵌套 iframe 加载一个静态页面,在 iframe 上加载的域名上存储设备id,通过跨域共享变量获取设备id,共享变量的原理是采用了iframe 的 contentWindow通讯,通过 postMessage 获取事件状态,调用封装好的回调函数进行数据处理具体的实现方式

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id,
collect.setIframe = function () {
var that = this
var iframe = document.createElement('iframe')
iframe.id = "frame",
iframe.src = 'http://collectiframe.trc.com' // 配置域名代理,目的是让开发测试生产环境代码一致
iframe.style.display='none' //iframe 设置的目的是用来生成固定的设备id,不展示
document.body.appendChild(iframe)

iframe.onload = function () {
iframe.contentWindow.postMessage('loaded','*');
}

//监听message事件,iframe 加载完成,获取设备id ,进行相关的数据采集
helper.on(window,"message",function(event){
that.deviceId = event.data.deviceId

if(event.data && event.data.type == 'loaded'){
that.sendDevice(that.getDevice(), that.deviceUrl);
setTimeout(function () {
that.send(that.beforeload)
that.send(that.loaded)
},1000)
}
})
}

iframe 与 SDK 通讯

function receiveMessageFromIndex ( event ) {
getDeviceInfo() // 获取设备信息
var data = {
deviceId: _deviceId,
type:event.data
}

event.source.postMessage(data, '*'); // 将设备信息发送给 SDK
}

//监听message事件
if(window.addEventListener){
window.addEventListener("message", receiveMessageFromIndex, false);
}else{
window.attachEvent("onmessage", receiveMessageFromIndex, false)

如果你想知道可以看我的另一篇博客 web 浏览器指纹跨域共享

单页面应用:现在流行的单页面应用和普通 web 页面的数据采集是否有差异

我们知道单页面应用都是无刷新的页面加载,所以我们在页面跳转的处理和我们的普通的页面会有所不同。单页面应用的路由插件运用了 window 自带的无刷新修改用户浏览记录的方法,pushState 和 replaceState。

window 的 history 对象 提供了两个方法,能够无刷新的修改用户的浏览记录,pushSate,和 replaceState,区别的 pushState 在用户访问页面后面添加一个访问记录, replaceState 则是直接替换了当前访问记录,所以我们只要改写 history 的方法,在方法执行前执行我们的采集方法就能实现对单页面应用的页面跳转事件的采集了

// 改写思路:拷贝 window 默认的 replaceState 函数,重写 history.replaceState 在方法里插入我们的采集行为,在重写的 replaceState 方法最后调用,window 默认的 replaceState 方法

collect = {}
collect.onPushStateCallback : function(){} // 自定义的采集方法

(function(history){
var replaceState = history.replaceState; // 存储原生 replaceState
history.replaceState = function(state, param) { // 改写 replaceState
var url = arguments[2];
if (typeof collect.onPushStateCallback == "function") {
collect.onPushStateCallback({state: state, param: param, url: url}); //自定义的采集行为方法
}
return replaceState.apply(history, arguments); // 调用原生的 replaceState
};
})(window.history);

这块介绍起来也比较的复杂,如果你想了解更多,可以看我的另一篇博客你需要知道的单页面路由实现原理

混合应用:app 与 h5 的混合应用我们要怎么进行通讯

现在大部分的应用都不是纯原生的应用, app 与 h5 的混合的应用是现在的一种主流。

纯 web 数据采集我们考虑到前端存储数据容易丢失,我们在每一次事件触发的时候都用采集接口传输采集到的数据。考虑到现在很多用户的手机会有流量管家的软件监控,如果在 App 中 h5 还是采集到数据就传输给服务端,很有可能会让流量管家检测到,给用户报警,从而使得用户不再信任你的 App , 所以我们在用户操作的时候将数据传给 app 端,存储到 app。用户切换应用到后台的时候,通过 app 端的 SDK 打包传输到服务器,我们给 app 提供的方法封装了一个适配器

// app 与 h5 混合应用,直接将数信息发给 app
collect.saveEvent = function (jsonString) {

collect.dcpDeviceType && setTimeout(function () {
if(collect.dcpDeviceType=='android'){
android.saveEvent(jsonString)
} else {
window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.nativeBridge.postMessage(jsonString) : window.postBridgeMessage(jsonString)
}

},1000)
}

实现思路

通过上面几个问题的思考,我们对埋点的实现大致已经有了一些想法,我们使用思维导图来还原下我们即将要做的事情,图片记得放大看哦,太小了可能看不清。

我们需要暴露给业务方调用的方法



我们来看下几个核心代码的实现

工具方法

我们定义了几个工具方法,提高开发的幸福指数 😝

var helper = {};

// 生成一个唯一的标识,pageSessionId (用这个变量来关联开始加载、加载完成、离开页面的事件,计算出页面加菜时间,停留时间)
helper.uuid = function(){}

// 元素绑定事件监听,兼容浏览器到IE8
helper.on = function(){}

//元素移除事件监听的适配器函数,兼容浏览器到IE8
helper.remove = function(){}

//将json转为字符串,事件传输的参数类型转化
helper.changeJSON2Query = function(){}

//将相对路径解析成文档全路径
helper.normalize = function(){}

采集逻辑

var collect = {
deviceUrl:'http://collect.trc.com/rest/collect/device/h5/v1',
eventUrl:'http://collect.trc.com/rest/collect/event/h5/v1',
isuploadUrl:'http://collect.trc.com/rest/collect/isupload/app/v1',
parmas:{ ExtraInfo:{} },
device:{}
};

//获取埋点配置
collect.setParames = function(){}

//更新访问路径及页面信息
collect.updatePageInfo = function(){}

//获取事件参数
collect.getParames = function(){}

//获取设备信息
collect.getDevice = function(){}

//事件采集
collect.send = function(){}

//设备采集
collect.sendDevice = function(){}

//判断才否采集,埋点采集的开关
collect.isupload = function(){

1. 判断是否采集,不采集就注销事件监听(项目中区分游客身份和用户身份的采集情况,这个方法会被判断两次)
2. 采集则判断是否已经采集过
a.已经采集过不做任何操作
b.没有采集过添加事件监听
3. 判断是 混合应用还是纯 web 应用
a.如果是web 应用,调用 collect.setIframe 设置 iframe
b.如果是混合应用 将开始加载和加载完成事件传输给 app
}

//点击事件处理函数
collect.clickHandler = function(){}

//离开页面的事件处理函数
collect.beforeUnloadHandler = function(){}

//页面回退事件处理函数
collect.onPopStateHandler = function(){}

//系统事件初始化,注册离开事件,浏览器后退事件
collect.event = function(){}

//获取记录开始加载数据信息
collect.getBeforeload = function(){}

//存储加载完成,获取设备类型,记录加载完成信息
collect.onload = function(){

1. 判断cookie是否有存设备类型信息,有表示混合应用
2. 采集加载完成时间等信息
3. 调用 collect.isupload 判断是否进行采集
}

//web 应用,通过嵌入 iframe 进行跨域 cookie 通讯,设置设备id
collect.setIframe = function(){}

//app 与 h5 混合应用,直接将数信息发给 app,判断设备类型做原生方法适配器
collect.saveEvent = function(){}

//采集自定义事件类型
collect.dispatch = function(){}

//将参数 userId 存入sessionStorage
collect.storeUserId = function(){}

//采集H5信息,如果是混合应用,将采集到的信息发送给 app 端
collect.saveEventInfo = function(){}

//页面初始化调用方法
collect.init = function(){

1. 获取开始加载的采集信息
2. 获取 SDK 配置信息,设备信息
3. 改写 history 两个方法,单页面应用页面跳转前调用我们自己的方法
4. 页面加载完成,调用 collect.onload 方法

}


collect.init(); // 初始化

//暴露给业务方调用的方法
return {
dispatch:collect.dispatch,
storeUserId:collect.storeUserId,
}

原文链接:https://segmentfault.com/a/1190000014922668


收起阅读 »

简易版 React-Router实现

上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router设计思路由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两...
继续阅读 »

上一篇简单的介绍了react-router 的使用方法和基本的API,对于react-router几个重要的API做了源码解读。这篇就实现一个简易版的 react-router

设计思路


由上图可知,核心内容就是如何监听到URL的改变?图中说到三种方式,其实也就两种pushstate 和 浏览器的前进和回退。刷新页面还是处于当前的URL,不涉及URL的改变。上一篇文章中也讲到 前端路由的原理有两点

  1. URL改变 页面不刷新。
  2. 监听到URL的改变。

所以在设计 react-router 的时候需要考虑 pushstate 和 浏览器的前进和回退这两种方式的URL改变。

Router

功能:负责监听页面对象发生了改变,并开始重新渲染页面 **

  1. 先定义一个上下文,方便把history数据传入所有的子组件
const RouteContext = React.createContext({})
  1. 定义 Router 组件,主要内容监听URL变化
const globalHistory = window.history // history 使用window 全局的history
class Router extends React.Component {
constructor(props) {
super(props)
this.state = { // 把location 设置为state 每次URL的改变,能够更新页面
location: window.location
}
// 第一种跳转方式:浏览器的前进后退,触发popstate 事件
window.addEventListener("popstate", () => {
this.setState({
location: window.location
})
})
}
// 第二种跳转方式:pushstate
// 向子组件提供push 方法更新路由,跳转页面
push = (route) => {
globalHistory.pushState({}, "", route)
this.setState({
location: window.location
})
}
// 定义上下文,把通用内容传入子组件
render() {
const { children } = this.props
const { location } = this.state
return (
<RouteContext.Provider value={{
history: globalHistory,
location,
push: this.push,
}}>
{
React.cloneElement(children, {
history: globalHistory,
location,
push: this.push,
})
}
</RouteContext.Provider>
)
}
}

export default Router

Route

功能:页面开始渲染后,根据具体的页面location信息展示具体路由地址对应的内容 **

import React, { useContext } from 'react'
const Route = (props) => {
// 在上下文中获取到相关信息
const context = useContext(RouteContext)
// 计算 location 匹配到的 path
const computedPath = (path, exact) => {
...TODO
// 这里内容和源码一样,其核心使用了path-to-regexp 库,能够计算出URL中的参数
}
// eslint-disable-next-line no-unused-vars
const { render, children, component, path, exact = false, ...rest } = props
const match = computedPath(path, exact)
const params = { ...context, match, location: context.location }
// 渲染 也就是源码中的三目运算。把相关的属性传入子组件
if (match) {
if (children) {
if (typeof children === 'function') {
return children(params)
}
return React.cloneElement(children, params)
} else if (component) {
return component(params)
} else if (render) {
return render(params)
}
}
return null
}

export default Route

这样一个简单的React-Router 就实现了,能够实现页面的跳转。

完整代码:https://github.com/LiuSandy/web

原文链接:https://zhuanlan.zhihu.com/p/366482879


收起阅读 »

React setState数据更新机制

为什么使用setState在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个de...
继续阅读 »

为什么使用setState

在React 的开发过程中,难免会与组件的state打交道。使用过React 的都知道,想要修改state中的值,必须使用内部提供的setState 方法。为什么不能直接使用赋值的方式修改state的值呢?我们就分析一下,先看一个demo。

class Index extends React.Component {
this.state = {
count: 0
}
onClick = () => {
this.setState({
count: 10
})
}
render() {
return (
<div>
<span>{this.state.count}</span>
<button onClick={this.onClick}>click</button>
</div>
)
}
}

根据上面代码可以看到,点击按钮后把state 中 count 的值修改为 10。并更新页面的显示。所以state的改变有两个作用:对应的值改变 和 页面更新。要想做到这两点在react 中 非 setState 不可。 假如说我们把 onClick 的方法内容修改为 this.state.count = 10 并在方法内打印出 this.state 的值,可以看到state的值已经改变。但是页面并没有更新到最新的值。 ☆总结一下:

  1. state 值的改变,目的是页面的更新,希望React 使用最新的 state来渲染页面。但是直接赋值的方式并不能让React监听到state的变化。
  2. 必须通过setState 方法来告诉React state的数据已经变化。

☆扩展一下:

在vue中,采用的就是直接赋值的方式来更新data 数据,并且Vue也能够使用最新的data数据渲染页面。这是为什么呢? 在vue2中采用的是 Object.defineProperty() 方式监听数据的get 和 set 方法,做到数据变化的监听 在vue3中采用的是ES6 的 proxy 方式监听数据的变化

setState 的用法

想必所有人都会知道setState 的用法,在这里还是想记录一下: setState方法有两个参数:第一个参数可以是对象直接修改属性值,也可以是函数能够拿到上一次的state值。第二个参数是一个可选的回调函数,可以获取最新的state值 回调函数会在组件更新完成之后执行,等价于在 componentDidUpdate 生命周期内执行。

  1. 第一个参数是对象时:如同上文的demo一样,直接修改state的属性值
this.setState({
key:newState
})
  1. 第一个参数是函数时:在函数内可以获取上一次state 的属性值。
// prevState 是上一次的 state,props 是此次更新被应用时的 props
this.setState((prevState, props) => {
return {
key: prevState.key
}
})

他们两者的区别主要体现在setState的异步更新上面!!!

异步更新还是同步更新

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式 将 setState() 视为请求而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。

先修改一下上面的代码,如果在onClick 方法中连续调用三次setState,根据上文可知 setState是一个异步的方式,每次调用只是将更改加入队列,同步调用的时候只会执行最后一次更新,所以结果是1而不是3。

onClick = () => {
const { count } = this.state
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
this.setState({ count: count + 1 })
}

可以把上面代码理解为 Object.assign() 方法,

Object.assign(
state,
{ count: state.count + 1 },
{ count: state.count + 1 },
{ count: state.count + 1 }
)

如果第一个参数传入一个函数,连续调用三次,是不是和传入对象方式的结果是一样的呢?

onClick = () => {
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
this.setState((prevState, props) => {
return {
count: prevState.count + 1
}
})
}

结果和传入对象的方式大相径庭,使用函数的方式就能够实现自增为3的效果。这又是为什么呢? 在函数内能够拿到最新的state 和 props值。由上文可知 setState 的更新是分批次的,使用函数的方式确保了当前state 是建立在上一个state 之上的,所以实现了自增3的效果。

☆总结一下: 为什么setState 方法是异步的呢?

  1. 可以显著的提升性能,react16 引入了 Fiber 架构,Fiber 中对任务进行了划分和优先级的分类,优先处理优先级比较高的任务。页面的响应就是一个优先级比较高任务,所以如果setState是同步,那么更新一次就要更新一次页面,就会阻塞到页面的响应。最好的办法就是获得到多个更新,之后进行批量的更新。只更新一次页面。
  2. 如果同步更新state,但是还没有执行render 函数,那么state 和 props 就不能够保持同步。

是不是所有的setState 都是异步的形式呢?答案是 否!!!在React 中也会存在setState 同步的场景

onClick = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
setTimeout(() => {
this.setState({ count: this.state.count + 1 })
console.log(this.state)
}, 0)
}

上面的代码会打印出0,2。这又是为什么呢?其实React 中的 setState 并不是严格意义上的异步函数。他是通过队列的延迟执行实现的。使用 isBatchingUpdates 判断当前的setState 是加入到更新队列还是更新页面。当 isBatchingUpdates=ture 是加入更新队列,否则执行更新。

知道了React 是使用 isBatchingUpdates 来判断是否加入更新队列。那么为什么在 setTimeout 事件中 isBatchingUpdates 值为 false ? 原因就是在React中,对HTML的原生事件做了一次封装叫做合成事件。所以在React自己的生命周期和合成事件中,可以控制 isBatchingUdates 的值,可以根据值来判断是否更新页面。而在宿主环境提供的原生事件中(即非合成事件),无法将 isBatchingUpdates 的值置为 false,所以就会立即执行更新。

☆所以setState 并不是有同步的场景,而是在特殊的场景下不受React 的控制 **

总结

setState 并不是单纯的同步函数或者异步函数,他的同步和异步的表现差异体现在调用的场景不同。在React 的生命周期和合成事件中他表现为异步函数。而在DOM的原生事件等非合成事件中表现为同步函数。

本节通过分析setState 的更新机制了解到setState 同步和异步的两种场景,下一节深入剖析下调用setState都做了什么?结合源码了解下为什么会出现两种场景?

原文:https://zhuanlan.zhihu.com/p/366781311

收起阅读 »

配置 ESLint 自动格式化自闭合标签(Self closing tag)

对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,- <SomeComponent></SomeComponent> + <SomeComponent/> 通过配置 ESLint 可在格式化...
继续阅读 »

对于没有子元素或不需要子元素的 HTML 标签,通常写成其自闭合的形式会显得简洁些,

- <SomeComponent></SomeComponent>
+ <SomeComponent/>

通过配置 ESLint 可在格式化的时候将标签自动变成自闭合形式。

create-react-app

如果是使用 create-react-app 创建的项目,直接在 package.json 的 eslint 配置部分加上如下配置即可:

"eslintConfig": {
"extends": "react-app",
+ "rules": {
+ "react/self-closing-comp": [
+ "error"
+ ]
}

安装依赖

安装 ESLint 相关依赖:

$ yarn add eslint eslint-plugin-react

如果是 TypeScript 项目,还需要安装如下插件:

$ yarn add @typescript-eslint/eslint-plugin  @typescript-eslint/parser

配置 ESLint

通过 yarn eslint --init 向导来完成创建,

或手动创建 .eslintrc.json 填入如下配置:

{
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["react", "@typescript-eslint"],
"rules": {
"react/self-closing-comp": ["error"]
}
}

安装 ESLint for Vscode

当然了,还需要安装 VSCode 插件 dbaeumer.vscode-eslint

然后配置 VSCode 在保存时自动进行修正动作:

"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},

使用

完成上述配置后,如果发现保存时,格式并未生效,或者只 JavaScript 文件生效,需要补上如下的 VSCode 配置:

"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
]

也可查看 VSCode 的状态栏,看是否有报错可确定是什么原因导致 ESLint 工作不正常,比如 mac BigSur 中细化了权限,需要点击警告图标然后点击允许。




原文:https://zhuanlan.zhihu.com/p/368639332

收起阅读 »

浅谈前端权限设计方案

前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可. 比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制...
继续阅读 »

前端权限架构的设计一直都是备受关注的技术点.通过给项目引入了权限控制方案,可以满足我们灵活的调整用户访问相关页面的许可.


比如哪些页面向游客开放,哪些页面必须要登录后才能访问,哪些页面只能被某些角色访问(比如超级管理员).有些页面即使用户登录了但受到角色的限制,他也只被允许看到页面的部分内容.


出于实际工作的需要,很多项目(尤其类后台管理系统)需要引入权限控制.倘若权限整体的架构设计的不好或者没有设计,会导致项目中各种权限代码混入业务代码造成结构混乱,其次想给新模块引入权限控制或者功能扩展都十分棘手.


虽然前端在权限层面能做一些事情,但很遗憾真正对权限进行把关的是后端.例如一个软件系统,前端在不写一行权限代码的情况下,当用户进入某个他无权访问的页面时,后端是可以判断他越权访问并拒绝返回数据的.由此可见前端即使不做什么整个系统也是可以正常运行的,但这样应用的体验很不好.另外一个很重要的原因就是前端做的权限校验都是可以被本地数据造假越权通过.


前端如果能判断某用户越权访问页面时,就不要让他进入那张页面后再弹出无权访问的信息提示,因为这样体验很差.最优方案是直接关闭那些页面的入口,只让他看到他能访问的页面.即使他通过输入路径恶意访问,导航最后只会将它带到默认页面或404页面.


前端做的权限控制大抵是先接受后台发送的权限数据,然后将数据注入到应用中.整个应用于是开始对页面的展现内容以及导航逻辑进行控制,从而达到权限控制的目的.前端做的权限控制虽然能提供一层防护,但根本目的还是为了优化体验.


本文接下来将从下面三个层面,从易到难步步推进,讲述目前前端主流的权限控制方案的实现.(下面代码将会以vue3vue-router 4演示)



  • 登录权限控制

  • 页面权限控制

  • 内容权限控制
登录权限控制

登录权限控制要做的事情,是实现哪些页面能被游客访问,哪些页面只有登录后才能被访问.在一些没有引入角色的软件系统中,通过是否登录来评定页面能否被访问在实际工作中非常常见.

实现这个功能也非常简单,首先按照惯例定义一份路由.

export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/list", // 列表页
name:"List",
component: List,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
}
]

假定存在三个页面:登录页、列表页和个人中心页.登录页和列表页所有人都可以访问,但个人中心页面需要登录后才能看到,给该路由添加一个meta对象,并将need_login置为true;


另外对于那些需要登录后才能看到的页面,用户如果没有登录就访问,就将页面跳转到登录页.等到他填写完用户名和密码点击登录后直接跳转到原来他想访问的页面.


在代码层面,通过router.beforeEach可以轻松实现上述目标,每次页面跳转时都会调用router.beforeEach包裹的函数,代码如下.


to是要即将访问的路由信息,从其中拿到need_login的值可以判断是否需要登录.再从vuex中拿到用户的登录信息.


如果用户没有登录并且要访问的页面又需要登录时就使用next跳转到登录页面,并将需要访问的页面路由名称通过redirect_page传递过去,在登录页面就可以拿到redirect_page等登录成功后直接跳转.

//vue-router4 创建路由实例
const router = createRouter({
history: createWebHashHistory(),
routes,
});

router.beforeEach((to, from, next) => {
const { need_login = false } = to.meta;
const { user_info } = store.state; //从vuex中获取用户的登录信息
if (need_login && !user_info) {
// 如果页面需要登录但用户没有登录跳到登录页面
const next_page = to.name; // 配置路由时,每一条路由都要给name赋值
next({
name: 'Login',
params: {
redirect_page: next_page,
...from.params, //如果跳转需要携带参数就把参数也传递过去
},
});
} else {
//不需要登录直接放行
next();
}
});

页面权限控制


页面权限控制要探讨的问题是如何给不同角色赋予不同的页面访问权限,接下来先了解一下角色的概念.


在一些权限设置比较简单的系统里,使用上面第一种方法就足够了,但如果系统引入了角色,那么就要在上面基础上,再进一步改造增强权限控制的能力.


角色的出现是为了更加个性化配置权限列表.比如当前系统设置三个角色:普通会员,管理员以及超级管理员.普通会员能够浏览软件系统的所有内容,但是它不能编辑和删除内容.管理员拥有普通会员的所有能力,另外它还能删除和编辑内容.超级管理员拥有软件系统所有权限,他单独拥有赋予某个账号为管理员或移除其身份的能力.


一旦软件系统引入了角色的概念,那么每个账户在注册之后就会被赋予相应的角色,从而拥有相应的权限.我们前端要做的事情就是依据不同角色给与它相应页面访问和操作的权限.这里要注意,前端依据的客体是角色,不是某个账户,因为账户是依托于角色的.


普通会员,管理员以及超级管理员这样角色的安排还是一种非常简单的划分方式,在实际项目中,角色的划份要更加细致的多.比如一些常见的后台业务系统,软件系统会按照公司的各个部门来建立角色,诸如市场部,销售部,研发部之类.公司的每个成员就会被划分到相应角色中,从而只具备该角色所拥有的权限.


公司另外一些高层领导他们的账户则会被划分到普通管理员或高级管理员中,那么他们相较于其他角色也会拥有更多的权限.


上面介绍那么多角色的概念其实是为了从全栈的维度去理解权限的设计,但真正落地到前端项目中是不需要去处理角色逻辑的,那部分功能主要由后端完成.


现在假定后端不处理角色完全交给前端来做会出现什么问题.首先前端新建一个配置文件,假定当前系统设定三种角色:普通会员,管理员以及超级管理员以及每个角色能访问的页面列表(伪代码如下).

export const permission_list = {
member:["List","Detail"], //普通会员
admin:["List","Detail","Manage"], // 管理员
super_admin:["List","Detail","Manage","Admin"] // 超级管理员
}

数组里每个值对应着前端路由配置的name值.普通会员能访问列表页详情页,管理员能额外访问内容管理页面,超级管理员能额外访问人员管理页面.


整个运作流程简述如下.当用户登录成功之后,通过接口返回值得知用户数据和所属角色.拿到角色值后就去配置文件里取出该角色能访问的页面列表数组,随后将这部分权限数据加载到应用中从而达到权限控制的目的.


从上面流程看,角色放在前端配置也是可以的.但假如项目已经上线,产品经理要求项目急需增加一个新角色合作伙伴,并把原来已经存在的用户张三移动到合作伙伴角色下面.那这样的变动会导致前端需要修改代码文件,在原来的配置文件上再新建角色来满足这一需求.


由此可见由前端来配置角色列表是非常不灵活且容易出错的,那么最优方案是交给后端去配置.用户一旦登录后,后端接口直接返回该账号拥有的权限列表就行了,至于该账户属于什么角色以及角色拥有的页面权限全部丢给后端去处理.


用户登录成功后,后端接口数据返回如下.

{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}

前端现在不需要理会张三属于什么角色,只需要按照张三的权限列表给他相应的访问权限就行了,其他都交给后端处理.

通过接口的返回值permission_list可知,张三能访问列表页详情页以及内容管理页.我们先回到路由配置页面,看看如何配置.

//静态路由
export const routes = [
{
path: '/login', //登录页面
name: 'Login',
component: Login,
},
{
path:"/myCenter", // 个人中心
name:"MyCenter",
component: MyCenter,
meta:{
need_login:true //需要登录
}
},
{
path:"/", // 首页
name:"Home",
component: Home,
}
]

//动态路由
export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
},
{
path:"/detail", // 详情页
name:"Detail",
component: Detail
},
{
path:"/manage", // 内容管理页
name:"Manage",
component: Manage
},
{
path:"/admin", // 人员管理页
name:"Admin",
component: Admin
}
]

现在将所有路由分成两部分,静态路由routes和动态路由dynamic_routes.静态路由里面的页面是所有角色都能访问的,它里面主要区分登录访问和非登录访问,处理的逻辑与上面介绍的登录权限控制一致.


动态路由dynamic_routes里面存放的是与角色定制化相关的的页面.现在继续看下面张三的接口数据,该如何给他设置权限.

{
user_id:1,
user_name:"张三",
permission_list:["List","Detail","Manage"]
}

用户登录成功后,一般会将上述接口信息存到vuexlocalStorage里面.假如此时刷新浏览器,我们就要动态添加路由信息.

import store from "@/store";

export const routes = [...]; //静态路由

export const dynamic_routes = [...]; //动态路由

const router = createRouter({ //创建路由对象
history: createWebHashHistory(),
routes,
});

//动态添加路由
if(store.state.user != null){ //从vuex中拿到用户信息
//用户已经登录
const { permission_list } = store.state.user; // 从用户信息中获取权限列表
const allow_routes = dynamic_routes.filter((route)=>{ //过滤允许访问的路由
return permission_list.includes(route.name);
})
allow_routes.forEach((route)=>{ // 将允许访问的路由动态添加到路由栈中
router.addRoute(route);
})
}

export default router;

核心代码在动态添加路由里面,主要利用了vue-router 4提供的APIrouter.addRoute,它能够给已经创建的路由实例继续添加路由信息.


我们先从vuex里面拿到当前用户的权限列表,然后遍历动态路由数组dynamic_routes,从里面过滤出允许访问的路由,最后将这些路由动态添加到路由实例里.


这样就实现了用户只能按照他对应的权限列表里的规则访问到相应的页面,至于那些他无权访问的页面,路由实例根本没有添加相应的路由信息,因此即使用户在浏览器强行输入路径越权访问也是访问不到的.


由于vue-router 4废除了之前的router.addRoutes,换成了router.addRoute.每一次只能一个个添加路由信息,所以要将allow_routes遍历循环添加.


动态添加路由这部分代码最好单独封装起来,因为用户第一次使用还没登录时,store.state.user是为空的,上面动态添加路由的逻辑会被跳过.那么在用户登录成功获取到权限列表的信息后,需要再把上面动态添加路由的逻辑执行一遍.


添加嵌套子路由


假如静态路由的形式如下,现在想把列表页添加到Tabs嵌套路由的children里面.

const routes = [
{
path: '/', //标签容器
name: 'Tabs',
component: Tabs,
children: [{
path: '', //首页
name: 'Home',
component: Home,
}]
}
]

export const dynamic_routes = [
{
path:"/list", // 列表页
name:"List",
component: List
}
]

官方router.addRoute给出了相应的配置去满足这样的需求(代码如下).router.addRoute接受两个参数,第一个参数对应父路由的name属性,第二个参数是要添加的子路由信息.

router.addRoute("Tabs", {
path: "/list",
name: "List",
component: List,
});

切换用户信息是非常常见的功能,但是应用在切换成不同账号后可能会引发一些问题.例如用户先使用超级管理员登录,由于超级管理员能访问所有页面,因此所有页面路由信息都会被添加到路由实例里.


此时该用户退出账号,使用一个普通会员的账号登录.在不刷新浏览器的情况下,路由实例里面仍然存放了所有页面的路由信息,即使当前账号只是一个普通会员,如果他越权访问相关页面,路由还是会跳转的,这样的结果并不是我们想要的.


解决方案有两个.第一是用户每次切换账户后刷新浏览器重新加载,刷新后的路由实例是重新配置的所以可以避免这个问题,但是刷新页面会带来不好的体验.


第二个方案是当用户选择登出后,清除掉路由实例里面处存放的路由栈信息(代码如下).

const router = useRouter(); // 获取路由实例
const logOut = () => { //登出函数
//将整个路由栈清空
const old_routes = router.getRoutes();//获取所有路由信息
old_routes.forEach((item) => {
const name = item.name;//获取路由名词
router.removeRoute(name); //移除路由
});
//生成新的路由栈
routes.forEach((route) => {
router.addRoute(route);
});
router.push({ name: "Login" }); //跳转到登录页面
};

移除单个路由主要利用了官方提供的API,即router.removeRoute.


路由栈清空后什么页面都不能访问了,甚至登录页面都访问不了.所以需要再把静态的路由列表routes引入进来,使用router.addRoute再添加进入.这样就能让路由栈恢复到最初的状态.


内容权限控制


页面权限控制它能做到让不同角色访问不同的页面,但对于一些颗粒度更小的项目,比如希望不同的角色都能进入页面,但要求看到的页面内容不一样,这就需要对内容进行权限控制了.


假设某个后台业务系统的界面如下图所示.表格里面存放的是列表数据,当点击发布需求时跳转到新增页面.当勾选列表中的某一条数据后,点击修改按钮显示修改该条数据的弹出框.同理点击删除按钮显示删除该条数据的弹出框


假设项目需求该系统存在三个角色:职员、领导和高层领导.职员不具备修改删除以及发布需求的功能,他只能查看列表.当职员进入该页面时,页面上只显示列表内容,其他三个按钮移除.


领导角色保留列表发布需求按钮.高级领导角色保留页面上所有内容.


我们拿到图片后要先要对页面内容整体分析一遍,按照增删查改四个维度对页面内容进行归类.使用简称CURD来标识(CURD分别代表创建(Create)、更新(Update)、读取(Retrieve)和删除(Delete)).


上图中列表内容属于查询操作,因此设定为R.凡是具备R权限的用户就显示该列表内容.


发布需求属于新增操作,设定凡是具备C权限的用户就显示该按钮.


同理修改按钮对应着U权限,删除按钮对应着D权限.


由此可以推断出职员角色在该页面的权限编码为R,它只能查看列表内容无法操作.


领导角色对应的权限编码为CR.高级领导对应的权限编码为CURD.


现在用户登录完成后,假设后端接口返回的数据如下(将这条数据存到vuex):

{
user_id:1,
user_name:"张三",
permission_list:{
"List":"CR", //权限编码
"Detail":"CURD" //权限编码
}
}

张三除了静态路由设置的页面外,他只能额外访问List列表页以及Detail详情页.其中列表页他只具备创建和新增权限,详情页他具备增删查改所有权限.那么当张三访问上图中的页面时,页面中应该只显示列表发布需求按钮.


我们现在要做的就是设计一个方案尽可能让页面内容方便被权限编码控制.首先创建一个全局的自定义指令permission,代码如下:

import router from './router';
import store from './store';

const app = createApp(App); //创建vue的根实例

app.directive('permission', {
mounted(el, binding, vnode) {
const permission = binding.value; // 获取权限值
const page_name = router.currentRoute.value.name; // 获取当前路由名称
const have_permissions = store.state.permission_list[page_name] || ''; // 当前用户拥有的权限
if (!have_permissions.includes(permission)) {
el.parentElement.removeChild(el); //不拥有该权限移除dom元素
}
},
});

当元素挂载完毕后,通过binding.value获取该元素要求的权限编码.然后拿到当前路由名称,通过路由名称可以在vuex中获取到该用户在该页面所拥有的权限编码.如果该用户不具备访问该元素的权限,就把元素dom移除.


对应到上面的案例,在页面里按照如下方式使用v-permission指令.

<template>
<div>
<button v-permission="'U'">修改</button> <button v-permission="'D'">删除</button>
</div>
<p>
<button v-permission="'C'">发布需求</button>
</p>

<!--列表页-->
<div v-permission="'R'">
...
</div>
</template>

将上面模板代码和自定义指令结合理解一下就很容易明白整个内容权限控制的逻辑.首先前端开发页面时要将页面分析一遍,把每一块内容按照权限编码分类.比如修改按钮就属于U,删除按钮属于D.并用v-permission将分析结果填写上去.


当页面加载后,页面上定义的所有v-permission指令就会运行起来.在自定义指令内部,它会从vuex中取出该用户所拥有的权限编码,再与该元素所设定的编码结合起来判端是否拥有显示权限,权限不具备就移除元素.


虽然分析过程有点复杂,但是以后每个新页面想接入权限控制非常方便.只需要将新页面的各个dom元素添加一个v-permission和权限编码就完成了,剩下的工作都交给自定义指令内部去做.


延伸



如果项目中删除操作并不是单独放置在一个按钮,而是与列表捆绑在一起放在表格的最后一列,如下图所示.

这样的界面样式在实际工作中非常常见,但似乎上面的v-permission就并不能友好的支持这样的样式.自定义指令在这种情况下虽然不能用,但我们仍然可以采用相同的思路去优化我们现有的代码结构.


例如模板代码如下.整个列表被封装成了一个组件List,那么在List内部就可以写很多的逻辑控制。


比如List组件内也可以通过vuex拿到该用户在当前页面的权限编码,如果发现具备D权限就显示列表中最后删除那一列,否则就不显示.至于整个列表的显示隐藏仍然可以使用v-permission来控制.

<template>
<div>
<button v-permission="'C'">添加资源</button>
</div>

<!--列表页-->
<List v-permission="'R'">
...
</List>
</template>

动态导航

下图中的动态导航也是实际工作中非常常见的需求,比如销售部所有成员只能看到销售模块下的两个页面,同理采购部成员只能看到采购模块下的页面.

下面侧边栏导航组件需要根据不同权限显示不同的页面结构,以满足不同角色群体的要求.

我们要把这种需要个性化设置的组件与上面使用v-permission控制的模式区分开.上面那些页面之所以能使用v-permission来控制,主要原因是因为产品经理在设计整个软件系统的页面时是按照增删查改的规则进行的.因此我们就能抽象出其中存在的共性与规律,再借助自定义指令来简化权限系统的开发.


但是侧边栏组件一般全局只有一个,没有什么特别的规律而言,那就只需要在组件内部使用v-if依据权限值动态渲染就行了.


比如后台接口如下:

{
user_id:1,
user_name:"张三",
permission_list:{
"SALE":true, //显示销售大类
"S_NEED":"CR", //权限编码
"S_RESOURCE":"CURD", //权限编码
}
}

张三拥有访问需求资源页面,但注意SALE并没有与哪个页面对应上,它仅仅只是表示是否显示销售这个一级导航.

接下来在侧面栏组件通过vuex拿到权限数据,再动态渲染页面就可以了.

<template>
<div v-if="permission_list['HOME']">系统首页</div>
<div v-if="permission_list['SALE']">
<p>销售</p>
<div v-if="permission_list['S_NEED']">需求</div>
<div v-if="permission_list['S_RESOURCE']">资源</div>
</div>
<div v-if="permission_list['PURCHASE']">
<p>采购</p>
<div v-if="permission_list['P_NEED']">需求</div>
<div v-if="permission_list['P_RESOURCE']">资源</div>
</div>
</template>

尾言

前端提供的权限控制为应用加固了一层保险,但同时也要警惕前端设定的校验都是可以通过技术手段破解的.权限问题关乎到软件系统所有数据的安危,重要性不言而喻.

为了确保系统平稳运行,前后端都应该做好自己的权限防护.

 原文链接:https://juejin.cn/post/6949453195987025927



收起阅读 »

超过 js 的 number 类型最大值(9007 1992 5474 0992)的解决办法

bug经过:点击修改无法展示信息(修改时调用queryOne,以id(long)为值,页面传过去的id=1480042498255640-00 ,在数据库中该id=148004249825564012,即错误的id)根本原因:js的number类型有个最大值(...
继续阅读 »

bug经过:点击修改无法展示信息(修改时调用queryOne,以id(long)为值,页面传过去的id=1480042498255640-00 ,在数据库中该id=148004249825564012,即错误的id)

根本原因:

js的number类型有个最大值(安全值)。即2的53次方,为9007199254740992。如果超过这个值,那么js会出现不精确的问题。这个值为16位。

解决方法:

1.后端发字符串类型。

将后端发过来的long类型转为string类型再向前端传。如果向前端传的是DAO集合,则每个DAO都需要转类型,太过于繁琐。想想就算了。

2.在userDao中加入一个字段

如果项目已经成型并且修改数据库会造成不可预料的问题那么可以在User对象中再增加一个String类型id映射字段,如下
    private Long userId;
    private String userIdStr;
    public String getUserIdStr() {
        return this.userId+"";
    }
    public void setUserIdStr(String userIdStr) {
        this.userIdStr = userIdStr;

    }

这个方法是比较靠谱的,确实可以正常的显示数据,查询单个数据id的值都是正确的。但修改用户时无法获取前端传过来的userDao中的userIdStr的值,因为上面的getUserIdStr()不能获取userIdStr的值(如果id没有值)。

3.控制用户新建数据时id的长度。兜兜转转觉得这个最方便。

温馨提示:以后设计表字段时尽量用varchar类型。

原文链接:https://blog.csdn.net/sunmerZeal/article/details/80844843


收起阅读 »

JavaScript 对象

为什么要有对象?如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据什么是对象?现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征举例:一部车,一...
继续阅读 »

为什么要有对象?

  • 如果有一组相关的数据,松散的存储不利于使用,存入数组中受下标限制又必须有固定的顺序,而对象可以自定义名称存储一系列无序列表的相关数据

什么是对象?

现实生活中的对象:万物皆可对象,对象是一个具体的事物,一个具体的事物就会有行为和特征

举例:一部车,一个手机

车是一类事物。,门口停的那辆车才是对象

特征:红色、四个轮子

行为:驾驶、刹车

  • JavaScript 中的对象:
  1. JavaScript 中的对象其实就是生活中对象的一个抽象
  2. JavaScript 的对象是无序属性的集合
  • 其属性可以包含基本值、对象或函数。对象就是一组没有顺序的值。我们可以把 JavaScript 中的对象想象成键值对,其中值可以是数据和函数
  • 对象的行为和特征:
  1. 特征---在对象中用属性表示
  2. 行为---在对象中用方法表示

对象字面量(用字面量创建对象)

  • 创建一个对象最简单的方法是使用对象字面量赋值给变量。类似数组
  • 对象字面量语法:{}
  • 内部可以存放多条数据,数据与数据之间用逗号分隔,最后一个后面不要加逗号
  • 每条数据都是有属性名和属性值组成,键值对写法: k : v
  • k:属性名
  • v:属性值,可以实任意类型的数据,比如简单类型数据、函数、对象
var obj = {
k:v,
k:v,
k:v,
};

区分属性和方法

  • 属性:对象的描述性特征,一般是名词,相当于定义在对象内部的变量
  • 方法:对象的行为和功能,一般是动词,定义在对象中的函数

调用对象内部属性和方法的语法

  • 用对象的变量名打点调用某个属性名,得到属性值
  • 在对象内部用 this 打点调用属性名。this 替代对象
  • 用对象的变量名后面加 [] 调用,[] 内部是字符串格式的属性名
  • 调用方法时,需要在方法名后加 () 执行
/* 
现实生活中:万物皆对象 对象是一个具体事物 看得见摸得着的实物

对象是一组无序的相关属性和方法的集合 所有事物的是对象

对象是由属性和方法组成的
属性:事物的特征 在对象中用属性来表示(常用名词)
方法:事物的行为 在对象中用方法来表示(常用动词)

对象的字面量:就是花括号{} 里面包含了表达这个具体实物(对象)的属性和方法
*/
//创建一个空的对象
var obj = {
uname:'张三',
age:'男',
sayHi: function () {
console.log('Hi!');
console.log(this.uname + "向你说您好");
}
}
// 1.我们在创建对象时我们采用键值对的形式 键 属性名 : 属性 属性值
// 2.多个属性或者方法中间用逗号隔开
// 3.方法冒号后面跟的是一个匿名函数

// 使用对象
// 1)调用对象的属性 我们采取 对象名.属性名
console.log(obj.uname);
// 2)调用对象也可以 对象名['属性名']
console.log(obj['age']);
// 3)调用对象的方法 对象.方法名
obj.sayHi();


更改对象内部属性和方法的语法

  • 更改属性的属性值方法:先调用属性,再等号赋值
obj.age = 19;
  • 增加新的属性和属性值:使用点语法或者 [] 方法直接定义新属性,等号赋值
obj.height = 180;
  • 删除一条属性:使用一个 delete 关键字,空格后面加属性调用
delete obj.sex;

new Object() 创建对象

  • object() 构造函数,是一种特殊的函数。主要用来再创建对象时初始化对象,即为对象成员变量赋值初始值,总与 new 运算符一起使用在创建对象的语句中
  1. 构造函数用于创建一类对象,首字母要大写
  2. 构造函数要和 new 一起使用才有意义
// 利用new object 创建对象
var obj = new Object();//创建了一个空对象
obj.name = '张三';
obj.age = 18;
obj.sex = '男';
obj.sayHi = function() {
console.log('Hi~');
}
//1.我们是利用等号赋值的方法给对象 属性和方法 赋值
//2.每个 属性和方法 用分号结束

// 调用
console.log(obj.name);
console.log(obj['sex']);
obj.sayHi();


new 在执行时会做四件事情

  • new 会在内存中创建一个新的空对象
  • new 会让 this 指向这个新的对象
  • 执行构造函数 目的 :给这个新对象加属性和方法
  • new 会返回这个新的对象

工厂 函数创建对象

  • 如果要创建多个类似的对象,可以将 new Object() 过程封装到一个函数中,将来调用函数就能创建一个对象,相当于一个生产对象的函数工厂,用来简化代码
// 我们为什么需要使用函数
// 就是因我们前面两种创建对象的方式一次只能创建一次对象
var ldh = {
uname: '刘德华',
age: 55,
sing = function() {
console.log('冰雨');
}
}
var zxy = {
uname: '张学友',
age: 58,
sing = function() {
console.log('李香兰');
}
}
// 因为我们一次创建一个对象 里面有很多的属性和方法是大量相同的 我们只能复制
// 因此我们可以利用函数的方法 重复这些相同的代码
// 又因为这个函数不一样 里面封装的不是普通代码 而是对象
// 函数 可以把我们对象里面一些相同的属性和方法抽象出来封装到函数里面

用 工厂方法 函数创建对象

function createStar(uname, age, sex) {
//创建一个空对象
var Star = new Object();
//添加属性和方法,属性可以接受参数的值
Star.name = uname;
Star.age = age;
Star.sex = sex;

Star.sing = function(sang) {
console.log(sang);
}
//将对象做为函数的返回值
return Star;
}

var p1 = createStar("张三",18,"男");

自定义构造函数

  • 比工厂方法更加简单
  • 自定义一个创建具体对象的构造函数,函数内部不需要 new 一个构造函数的过程,直接使用 this 代替对象进行属性和方法的书写,也不需要 return 一个返回值
  • 使用时,利用 new 关键字调用自定义的构造函数即可
  • 注意:构造函数的函数名首字母需要大写,区别于其他普通函数名
// 利用构造函数创建对象
// 我们需要创建四大天王的对象 相同的属性: 名字 年龄 性别 相同的方法 : 唱歌
// 构造函数的语法格式
/*
function 构造函数名() {
this.属性 = 值;
this.方法 = fucntion() {}
}
// 调用构造函数
new 构造函数名();
*/

function Star(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;

this.sing = function(sang) {
console.log(sang);
}
}
var ldh = new Star('刘德华', 18, '男');
console.log(typeof ldh);//object
console.log(ldh.name);
console.log(ldh.age);
console.log(ldh.sex);
ldh.sing('冰雨');
// 1.构造函数首字母必须大写
// 2.构造函数不需要return就能返回结果
// 3.调用函数返回的是一个对象
var zxy = new Star('张学友', 29, '男')
console.log(zxy);
// 4.我们调用构造函数必须使用new

对象遍历

  • for in 循环也是循环的一种,专门用来遍历对象,内部会定义一个 k 变量,k 变量在每次循环时会从第一个开始接收属性名,一直接收到最后一个属性名,执行完后会跳出循环。
  • 简单的循环遍历:输出每一项的属性名和属性值
//循环遍历输出每一项
for(var k in obj){
console.log(k + "项的属性值" + obj[k]);
}

案例:

//遍历对象
var obj = {
uname: '王二狗',
age: 18,
sex: '男'
}
console.log(obj.uname);
console.log(obj.age);
console.log(obj.sex);
//但是一个一个输出很累

// 因此我们引出 for...in...语句 --用于对数组或者对象的属性进行循环操作

/*
基本格式:
for (变量 in 对象) {

}

*/
for (k in obj) {
console.log(k); //k变量输出 得到的是属性名
console.log(obj[k]); //obj[k] 输出对象各属性的属性值 切记不要用obj.k 那样就变成输出 k 属性名的属性值了 ---!!!:k是变量不加''
}

简单类型和复杂类型的区别

  • 我们已经学过简单类型数据和一些复杂类型的数据,现在来看一下他们之间的区别有哪些
  • 基本类型又叫做值类型,复杂类型又叫做引用类型
  • 值类型:简单数据类型,基本数据类型,在存储时,变量中存储的是值本身,因此叫做值类型
  • 引用类型:复杂数据类型,在存储时,变量中存储的仅仅是地址(引用),因此叫做引用数据类型

堆和栈

  • JavaScript 中没有堆和栈的概念,此处我们用堆和栈来讲解,目的是方便理解和方便以后的学习
  • 堆栈空间分配区别
  1. 栈(操作系统):由操作系统自动分配释放,存放函数的参数值,局部变量的值相等
  2. 堆(操作系统):存储复杂类型(对象),一般由程序员分配释放,若程序员不释放,有垃圾回收机制回收

简单数据类型(基本类型)在内存中的存储

变量中如果存储的是简单类型的数据,那么变量中存储的是值本身,如果将变量赋值给另一个变量,是将内部的值赋值一份给了另一个变量,两个变量之间没有联系,一个变化,另一个不会同时变化

var a = 5;
var b = a; //将 a 内部存储的数据 5 复制了一份
a = 10;
console.log(a);
console.log(b);
// 因此 a 和 b 发生改变,都不会互相影响


复杂数据类型(引用类型)在内存中的存储

如果将复杂数据赋值给一个变量,复杂类型的数据会在内存中创建一个原型,而变量中存储的是指向对象的一个地址,如果将变量赋值给另一个变量,相当于将地址复制一份给了新的变量,两个变量的地址相同,指向的是同一个原型,不论通过哪个地址更改了原型,都是在原型上发生的更改,两个变量下次访问时,都会发生变化

// 复杂数据类型
var p1 = {
name: "zs",
age: 18,
sex: "male"
}
var p = p1; //p1 将内部存储的指向对象原型的地址复制给了 p
// 两个变量之间是一个联动的关系,一个变化,会引起另一个变化
p.name = "ls";
console.log(p);
console.log(p1);

// 数组和函数存储在变量中时,也是存储的地址
var arr = [1,2,3,4];
var arr2 =arr;
arr[4] = 5;
console.log(arr);
console.log(arr2);

内置对象

  • JavaScript 包含:ECMA DOM BOM
  • ECMAscript 包含:变量、数据、运算符、条件分支语句、循环语句、函数、数组、对象···
  • JavaScript 的对象包含三种:自定义对象 内置对象 浏览器对象
  • ECMAscript 的对象:自定义对象 内置对象
  • 使用一个内置对象,只需要知道对象中有哪些成员,有什么功能,直接使用
  • 需要参考一些说明手册 W3C / MDN

MDN

Mozilla 开发者网络(MDN) 提供有关开放网络技术(Open Web)的信息,包括 HTML、CSS 和 万维网 HTML5 应用的API

如何学习一个方法?

  1. 方法的功能
  2. 参数的意义和类型
  3. 返回值意义和类型
  4. demo 进行测试

Math 对象

  • Math 对象它具有数学常数和函数的属性和方法,我们可以直接进行使用
  • 根据数学相关的运算来找 Math 中的成员(求绝对值,取整)

演示:

Math.PI圆周率
Math.random()生成随机数
Math.floor()/Math.ceil()向下取整/向上取整
Math.round()取整,四舍五入
Math.abs()绝对值
Math.max()/Math.min()求最大和最小值
Math.sin()/Math.cos()正弦/余弦
Math.power()/Math.sqrt()求指数次幂/求平方根

Math.random()

如何求一个区间内的随机值

Math.random()*(max_num - min_num) + min_num

Math.max()/Math.min()

// Math数学对象 不是一个构造函数 所以我们不需要用new来调用 而是直接使用里面的属性和方法即可
console.log(Math.PI); //一个属性值 圆周率
console.log(Math.max(99, 199, 299)); //299
console.log(Math.max(-10, -20, -30)); //-10
console.log(Math.max(-10, -20, '加个字符串')); //NaN
console.log(Math.max()); //-Infinity
console.log(Math.min(99, 199, 299)); //99
console.log(Math.min()); //Infinity

创建数组对象的第二种方式

字面量方式

new Array() 构造函数方法

// 字面量方法
// var arr = [1,2,3];

// 数组也是对象,可以通过构造函数生存
//空数组
var arr = new Array();
//添加数据,可以传参数
var arr2 = new Array(1,2,3);
var arr3 = new Array("zs","ls","ww");
console.log(arr);
console.log(arr2);
console.log(arr3);

// 检测数组的数据类型
console.log(typeof(arr));//object
console.log(typeof(arr2));//object
console.log(typeof(arr3));//object

由于 object 数据类型的范围较大,所以我们需要一个更精确的检测数据类型的方法

  • instanceof 检测某个实例是否时某个对象类型
var arr = [1,2,3];
var arr1 = new Array(1,2,3)
var a = {};
// 检测某个实例对象是否属于某个对象类型
console.log(arr instanceof Array);//true
console.log(arr1 instanceof Array);//true
console.log(a instanceof Array);//true

function fun () {
console.log(1);
}
console.log(fun instanceof Function);//true

数组对象的属性和方法

toString()

  • toString() 把数组转换成字符串,逗号分隔每一项
// 字面量方法
var arr = [1,2,3,4];

// toString() 方法:可以转字符串
console.log(arr.toString());//1,2,3,4

数组常用方法

首尾数据操作:

  • push() 在数组末尾添加一个或多个元素,并返回数组操作后的长度
// 字面量方法
var arr = [1,2,3,4];

// 首尾操作方法
// 尾推,参数是随意的,有一个或者多个
console.log(arr.push(5,6,7,8)); //8(数组长度)
console.log(arr);//[1,2,3,4,5,6,7,8]
console.log(arr.push([5,6,7,8])); //5(数组长度)
console.log(arr);//[1,2,3,4,Array(4)]
  • pop() 删除数组最后一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];

//尾删,删除最后一项数据
// 不需要传参
console.log(arr.pop());//4(被删除的那一项数据)
console.log(arr);//[1,2,3]
  • shift() 删除数组第一项,返回删除项
// 字面量方法
var arr = [1,2,3,4];

// 首删,删除第一项数据,不需要传参
console.log(arr.shift());//1
console.log(arr);//[2,3,4]
  • unshift() 在数组开头添加一个或多个元素,并返回数组的长度
// 字面量方法
var arr = [1,2,3,4];

// 首添,参数与 push 方法类似
console.log(arr.unshift(-1,0));//6
console.log(arr);//[-1,0,1,2,3,4]


案例:将数组的第一项移动到最后一项

// 字面量方法
var arr = [1,2,3,4];

// 将数组的第一项移动到最后一项
// 删除第一项
// 将删除的项到最后一项
arr.push(arr.shift());
console.log(arr);//[2,3,4,1]
arr.push(arr.shift());
console.log(arr);//[3,4,1,2]
arr.push(arr.shift());
console.log(arr);//[4,1,2,3]
arr.push(arr.shift());
console.log(arr);//[1,2,3,4]


数组常用方法

合并和拆分:

concat()

  • 将两个数组合并成一个新的数组,原数组不受影响。参数位置可以是一个数组字面量、数组变量、零散的值
// 字面量方法
var arr = [1,2,3,4];
// 合并方法
// 参数:数组 数组的变量 零散的值
// 返回值:一个新的拼接后的数组
var arr1 = arr.concat([5,6,7]);
console.log(arr);//[1,2,3,4]
console.log(arr1);//[1,2,3,4,5,6,7]

slice(start,end)

  • 从当前数组中截取一个新的数组,不影响原来的数组,返回一个新的数组,包含从 start 到end (不包括该元素)的元素
  • 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾
// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取


删除,插入,替换:

splice(index,howmany,element1,element2,...)

用于插入、删除或替换数组的元素

index:删除元素的开始位置

howmany:删除元素的个数,可以是0

element1,element2:要替换的新数据

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 拆分方法
// 参数为正
var arr1 = arr.slice(3,7);//[4,5,6,7]
// 参数为负数
var arr1 = arr.slice(-7,-1);//[5,6,7,8,9]
// 参数出现问题的情况
var arr1 = arr.slice(-1,-7);//[] 取不到会出现空值
// 只书写一个参数
var arr1 = arr.slice(-7);//[4,5,6,7,8,9,10] 从倒数第七个开始取
var arr1 = arr.slice(8);//[9,10] 从下标为8的数开始取


位置方法:

indexOf() 查找数据在数组中最先出现的下标

lastndexOf() 查找数据在数组中最后一次出现的下标

注意:如果没有找到返回-1

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,4,5];

// 查找某个元素在数组中从前往后第一次 出现位置的下标
console.log(arr.indexOf(4));//3 (数字4的下标)
// 查找某个元素在数组中从前往后最后一次出现位置的下标
console.log(arr.lastIndexOf(4));//10
console.log(arr.lastIndexOf(11));//-1 (代表数组中不存在11这个数据)


排序方法:

倒序:reverse() 将数组完全颠倒,第一项变成最后一项,最后一项变成第一项

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10];

// 数组倒序
console.log(arr.reverse());//[10,9,8,7,6,5,4,3,2,1]

从大到小排序:sort() 默认根据字符编码顺序,从大到小排序

如果想要根据数值大小进行排序,必须添加sort的比较函数参数

该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数具有两个参数 a 和 b,根据 a 和 b 的关系作为判断条件,返回值根据条件分为三个分支,整数、负数、0:

返回值是负数-1:a 排在 b 前面

返回值是整数1:a 排在 b 后面

返回值是0:a 和 b 的顺序保持不变

人为控制的是判断条件

// 字面量方法
var arr = [1,2,3,4,5,6,7,8,9,10,20,30];
// 排序,默认按照字符编码顺序来排序
arr.sort();
console.log(arr);//[1,10,2,20,3,30,4,5,6,7,8,9] (如果不添加函数)

// 添加一个比较函数
arr.sort(function(a,b) {
if (a > b) {
return -1;//表示 a 要排在 b 前面
} else if (a < b) {
return 1;//表示 a 要排在 b后面
} else {
return 0;;//表示 a 和 b 保持原样,不换位置
}
});
console.log(arr);//[30,20,10,9,8,7,6,5,4,3,2,1] (添加函数之后)
// 想要从小到大排序只要将函数 大于小于 号,反向即可


转字符串方法:将数组的所有元素连接到一个字符串中

join() 通过参数作为连字符将数组中的每一项用连字符连成一个完整的字符串

var arr = [1,2,3,4,5,6,7,8,9,10,20,30];

// 转字符串方法
var str = arr.join();
console.log(str);//1,2,3,4,5,6,7,8,9,10,20,30
var str = arr.join("*");
console.log(str);//1*2*3*4*5*6*7*8*9*10*20*30
var str = arr.join("");
console.log(str);//123456789102030


清空数组方法总结

var arr = [1,2,3,4,5,6,7,8,9,10,20,30];

// 方式1 推荐
arr = [];

// 方式2
arr.length = 0;

// 方式 3
arr.splice(0,arr.length);

基本包装类型

为了方便操作简单数据类型,JavaScript 还提供了特殊的简单类型对象:String 基本类型时没有方法的。

当调用 str.substring() 等方法的时候,先把 str 包装成 String 类型的临时对象,再调用 substring 方法,最后销毁临时对象

// 基本数据类型:没有属性值和方法
// 对象数据类型:有属性和方法
// 但是:字符串是可以调用一些属性和方法
var str = "这是一个字符串";
var str2 = str.slice(3,5);
console.log(str2);//个字

// 基本包装类型,基本类型的数据在进行一些特殊操作时,会暂时被包装成一个对象,结束后再被销毁
// 字符串也有一种根据构造函数创建方法
var str3 = new String("abcdef");
console.log(str);//这是一个字符串
console.log(str3);//Sring{"abcdef"}

// 模拟计算机的工作
var str4 = new String(str);
// 字符串临时被计算机包装成字符串对象
var str2 = str4.slice(3,5);
str4 = null;


字符串的特点

字符串是不可变的

// 定义一个字符串   
var a = "abc";
a = "cde";
// 字符串是不可变的,当 a 被重新赋值时,原来的值 "abc" 依旧在电脑内存中
// 在 JavaScript 解释器 固定时间释放内存的时候可能会被处理掉

由于字符串的不可变,在大量拼接字符串的时候会有效率问题

由于每次拼接一个字符串就会开辟一个空间去存储字符串

// 大量拼接字符串也效率问题
var sum = "";
for(var i = 1; i <= 10000000; i++) {
sum += i;
}
console.log(sum);

测试一下我们发现,浏览器转了一会才显示出来

因此在我们以后,不要大量用字符串拼接的方法,以后我们会有更好的方法替代


字符串属性

长度属性:str.length

字符串长度指的是一个字符串中所有的字符总数


字符串方法

indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置

  • 找到指定的字符串在原字符串中第一次出现的位置的下标。如果子字符串在原字符串中没有,返回值是 -1

concat() 方法用于连接两个或多个字符串

  • 参数比较灵活,可以是字符串、或者字符串变量、多个字符串
  • 生成的是一个新的字符串,原字符串不发生变化

split() 方法用于把一个字符串分割成字符串数组(和数组中的 join() 方法是对应的)

  • 参数部分是割符,利用分割符将字符串分割成多个部分,多个部分作为数组的每一项组成数组
  • 如果分割符是空字符串,相当于将每个字符拆分成数组中的每一项
// 定义一个字符串
var str = "这是一个字符串,abc, $%#";

// 长度属性
console.log(str.length);//18

// charAt() 返回指定的下标位置的字符
console.log(str.charAt(6));//串 (字符串对象是一种伪数组,所以需要从 0 开始数)

// indexOf() 返回子串在原始字符串中第一次出现位置的下标
console.log(str.indexOf("字"));//4
console.log(str.indexOf("字符串"));//4
console.log(str.indexOf("字 符串"));//-1

// concat() 字符串拼接
var str2 = str.concat("哈哈哈","普通");
console.log(str);//这是一个字符串,abc, $%#
console.log(str2);//这是一个字符串,abc, $%#哈哈哈普通

// split() 分割字符串成一个数组
var arr = str.split("")//一个一个字符分割
console.log(arr);
var arr = str.split(",")//按逗号进行分割
console.log(arr);

// 字符串内容倒置
var arr = str.split("")//一个一个字符分割
arr.reverse();
strn = arr.join("");
console.log(strn);
// 用连续打点方式化简
var arr = str.split("").reverse().join("")
console.log(arr);


toLowerCase() 把字符串转换为小写

toUpperCase() 把字符串转换为大写

  • 将所有的英文字符转为大写或者小写
  • 生成的是新的字符串,原字符串不发生变化
// 大小写转换
var str1 = str.toUpperCase();
console.log(str);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%#
var str2 = str1.toLowerCase();
console.log(str2);//这是一个字符串,abc, $%#
console.log(str1);//这是一个字符串,ABC, $%# --字符串本身不会发生改变


截取字符串的三种方法

slice() 方法可以提取字符串的某个部分,并以新的字符串返回被提取的部分

  • 语气:slice(start,end)
  • 从开始位置截取到结束位置(不包括结束位置)的字符串
  • 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,参数可以只传递一个,表示从开始位置截取到字符串结尾

substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符

  • 语法:substr(start,howmany)
  • 从开始位置截取到指定长度的字符串
  • start 参数区分正负。正值表示下标位置,负值表示从后往前数第几个位置
  • howmany 参数必须为正数,也可以不写,不写表示从 start 截取到最后

substring() 方法用于提取字符串中介于两个指定下标之间的字符

  • 语法:substring(start,end)
  • 参数只能为正数
  • 两个参数都是指代下标,两个数字大小不限制,执行方法之前会比较一下两个参数的大小,会用小的数当做开始位置,大的当作结束位置,从开始位置截取到结束位置但是不包含结束位置
  • 如果不写第二个参数,从开始截取到字符串结尾
// 截取字符串:三种
// slice(start,end) 从开始位置截取到结束位置,但是不包含结束位置
var str1 = str.slice(3,7);
console.log(str1);//个字符串
var str1 = str.slice(-7);
console.log(str1);//, $%#

// substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符
var str2 = str.substr(6);
console.log(str2);//串,abc, $%#
var str2 = str.substr(6,3);
console.log(str2);//串,a

// substring() 参数必须为整数 小的数当做开始位置,大的当作结束位置
var str3 = str.substring(3,7);
console.log(str3);//个字符串

注意:如果参数取小数会自动省略小数部分

原文链接:https://zhuanlan.zhihu.com/p/366886609

收起阅读 »

JavaScript 函数

为什么要有函数?如果要在多个地方求某个数的约数个数,应该怎么做函数的概念函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行...
继续阅读 »

为什么要有函数?

  • 如果要在多个地方求某个数的约数个数,应该怎么做


函数的概念

  • 函数(function),也叫作功能、方法,函数可以将一段代码一起封装起来,被封装起来的函数具备某一项特殊的功能,内部封装的一段代码作为一个完整的结构体,要执行就都执行,要不执行就都不执行。
  • 函数的作用就是封装一段代码,将来可以重复使用

函数声明

  • 函数声明又叫函数定义,函数必须先定义然后才能使用
  • 如果没有定义函数直接使用,会出现一个引用错误
  • 函数声明语法:
function 函数名 (参数) {
封装的结构体;
}

特点:函数声明的时候,函数体并不会执行,只有当函数被调用的时候才会执行

函数调用

  • 调用方法:函数名();
  • 函数调用也叫作函数执行,调用时会将函数内部封装的所有的结构体的代码立即执行
  • 函数内部语句执行的位置,与函数定义的位置无关,与函数调用位置有关
  • 函数可以一次调用,多次执行

函数的参数1

  • 我们希望函数执行结果不是一成不变的,可以根据自定义的内容发生一些变化
  • 函数预留了一个接口,专门用于让用户自定义内容,使函数发生一些执行效果变化
  • 接口:就是函数的参数,函数参数的本质就是变量,可以接收任意类型的数据,导致函数执行结果根据参数不同,结果也不同
  • 一个函数可以设置 0 个或者多个参数,参数之间用逗号分隔

案例:累加求和函数

        // 函数:封装了一段可以重复调用执行的代码块,通过代码块可以实现大量代码的重复使用

// 1、声明一个累加求和函数

// num1~num2之间所有数之和
function getSum(num1,num2) {
var sum = 0;
for (var i = num1; i <= num2; i++) {
sum += i;
}
console.log(sum);
}

// 2、调用函数
getSum(1,100);
getSum(11,1100);
getSum(321,1212);

函数的参数2

  • 函数的参数根据书写位置不同,名称也不相同
  • 形式参数:定义的 () 内部的参数,叫做形式参数,本质是变量,可以接收实际参数传递过来的数据。简称形参
  • 实际参数:调用的 () 内部的参数,叫做实际参数,本质就是传递的各种类型的数据,传递给每个形参,简称实参
  • 函数执行过程,伴随传参的过程

函数的参数优点

  • 不论使用自己封装的函数,还是其他人封装的函数,只需要知道传递什么参数,执行什么功能,没必要知道内部的结构是什么
  • 一般自己封装的函数或者其他人封装的函数需要有一个 API 接口说明,告诉用户参数需要传递什么类型的数据,实现什么功能

函数的返回值

  • 函数能够通过参数接收数据,也能够将函数执行结果返回一个值
  • 利用函数内部的一个 return 的关键字设置函数的返回值
  • 作用 1 :函数内部如果结构体执行到一个 return 的关键字,会立即停止后面代码的执行
  • 作用 2 : 可以在 return 关键字后面添加空格,空格后面任意定义一个数据字面量或者表达式,函数在执行完自身功能之后,整体会被 return 矮化成一个表达式,表达式必须求出一个值继续可以参加程序,表达式的值就是 return 后面的数据

案例:求和函数

var num1 = Number(prompt("请输入第一个数:"));
var num2 = Number(prompt("请输入第二个数:"));
function sum(a,b) {
return a + b;
}
console.log(sum(num1,num2));

函数的返回值应用

  • 函数如果有返回值,执行结果可以当成普通函数参与程序
  • 函数如果有返回值,可以作为一个普通数据赋值给一个变量,甚至赋值给其他函数的实际参数
  • 注意:如果函数没有设置 return 语句,那么函数有默认的返回值 undefined ; 如果函数使用 return 语句,但是 return 后面没有任何值,那么函数的返回值也是 undefined
// 1、return 终止函数
function getSum(num1, num2) {
return num1 + num2;
console.log('return除了返回值还起到终止函数的作用,所以在return后面的代码均不执行!');
}
console.log(getSum(10, 20));

// 2、return 只能返回一个值
function fn(num1,num2) {
return num1, num2; //返回的结果是最后一个值
}
console.log(fn(10, 20));

// 3、 我们求任意两个数 加减乘除 的结果
function getResult(num1, num2) {
return ['求和:' + (num1 + num2), '求差:' + (num1 - num2), '求积:' + (num1 * num2), '求商:' + (num1 / num2)];
}
re = getResult(10, 20);
console.log(re);
// 想要输出多个值可以利用数组
// 4、我们的函数如果有return 则返回的是 return后面的值 如果函数没有 return 则返回undefined

函数表达式

  • 函数表达式是函数定义的另外一种方式
  • 定义方法:就是将函数的定义、匿名函数赋值给一个变量
  • 函数定义赋值给一个变量,相当于将函数整体矮化成了表达式
  • 匿名函数:函数没有函数名
  • 调用函数表达式,方法是给变量名加 () 执行,不能使用函数名加 () 执行
// 函数的两种声明方式
// 1、利用函数关键字自定义函数(命名函数)
function fn() {

}
fn();
// 2、函数表达式(匿名函数)
// var 变量名 = function() {};
var fun = function(aru) {
console.log('我是函数表达式');
console.log(aru);
}
fun('我是默默!');
// (1)fun是变量名 不是函数名
// (2)函数表达式声明方式跟声明变量差不多,只不过变量里面存的是值 而 函数表达式里面存的是函数

函数数据类型

  • 函数是一种独特的数据类型 function -- 是 object 数据类型的一种,函数数据类型
  • 由于函数是一种数据类型,可以参与其他程序
  • 例如,可以把函数作为另外一个函数的参数,在另一个函数中调用
  • 或者,可以把函数作为返回值从函数内部返回
// 函数是一种数据类型,可以当成其他函数的参数
setInterval(function() {
console.log(1);
},1000)
//每隔 1s 输出一个 1

arguments 对象

  • JavaScript 中,arguments 对象是比较特别的一个对象,实际上是当前函数的一个内置属性。也就是说所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有实参。arguments 是一个伪数组,因此及可以进行遍历
  • 函数的实参个数和形参个数可以不一致,所有的实参都会存储在函数内部的 arguments 类数组对象中
/*
当我们不确定有多少个参数传递的时候 可以用arguments来获取 在JS中 arguments其实是当前函数的
一个内置对象 所有函数都内置了一个arguments对象 arguments对象中存储了传递的所有实参
*/
// arguments的使用
function fn() {
console.log(arguments); //里面存储了所有的实参
}
fn(1, 2, 3);

/*
arguments展示形式是一个伪数组,因此可以进行遍历,伪数组有如下特点:
具有length属性
按照索引方式存储数据
不具有数组的 push pop 等方法
*/

案例:利用 arguments 求一组数最大值

function getMax() {
var max = arguments[0];
var arry = arguments;
for (var i = 0; i < arry.length; i++) {
if (arry[i] > max) {
max = arry[i];
}
}
return max;
}

console.log(getMax(1, 2, 5, 11, 3));
console.log(getMax(1, 2, 5, 11, 3, 100, 111));
console.log(getMax(1, 2, 5, 11, 3, 1212, 22, 222, 2333));


函数递归

  • 函数内部可以通过函数名调用函数自身的方式,就是函数递归现象
  • 递归的次数太多容易出现错误:超出计算机的计算最大能力
  • 更多时候,使用递归去解决一些数学的现象
  • 例如可以输出斐波那契数列的某一项的值
// 函数,如果 传入的参数1,返回1,如果传入的是 1 以上的数字,让他返回参数 + 函数调用上一项
function fun (a) {
if (a === 1) {
return 1;
} else {
return a + fun(a - 1);
}
}
// 调用函数
console.log(fun(1));
console.log(fun(2));
console.log(fun(3));
console.log(fun(100));
// 这样我们就用递归做出了 n 以内数累加求和的函数

案例:输出斐波那契数列任意项

// 斐波那契数列(每一项等于前两项之和 1,1,2,3,5,8,13,21,34,55 ···)
// 参数:正整数
// 返回值:对应的整数位置的斐波那契数列的值
function fibo(a) {
if (a === 1 || a === 2) {
return 1;
} else {
return fibo(a - 1) + fibo(a - 2);
}
}
console.log(fibo(1));
console.log(fibo(2));
console.log(fibo(3));
console.log(fibo(4));


作用域

  • 作用域:变量可以起作用的范围
  • 如果变量定义在一个函数内部,只能在函数内部被访问到,在函数外部不能使用这个变量,函数就是变量定义的作用域
  • 任何一对花括号 {} 中的结构体都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域
  • 在 es6 之前没有块级作用域的概念,只有函数作用域,现阶段可以认为 JavaScript 没有块级作用域
// js现阶段没有块级作用域 js作用域:局部作用域 全局作用域 现阶段我们js没有块级作用域
// js在ES6的时候新增块级作用域的概念
// 块级作用域就是{}中的区域

if (3 > 2) {
var num1 = 10;
}
console.log(num1);//10

/*
说明js没有块级作用域,外部可以调用{}内声明的变量
*/

全局变量和局部变量

  • 局部:变量:定义在函数内部的变量,只能在函数作用域被访问到,在外面没有定义的
  • 全局变量:从广义上来说,也是一种局部变量,定义在全局的变量,作用域范围是全局,
  • 在整个 js 程序任意位置都能被访问到
  • 局部变量退出作用域之后会被销毁,全局变量关闭页面或浏览器才会销毁

函数参数也是局部变量

  • 函数的参数本质是一个变量,也有自己的作用域,函数的参数也是属于函数自己内部的局部变量,只能在函数内部被使用,在函数外面没有定义

函数的作用域

  • 函数也有自己的作用域,定义在哪个作用域内部,只能在这个作用域范围内被访问,出了作用域不能被访问
  • 函数定义在另一个函数内部,如果外部函数没有执行时,相当于内部代码没写

作用域链

  • 只有函数可以制造作用域结构,那么只要是代码,就至少有一个作用域,即全局作用域。凡是代码中有函数,那么这个函数就构成另一个作用域。如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用领域
  • 将这样的所有的作用域列出来,可以有一个结构:函数内指向函数外的链式结构。就称作作用域链

遮蔽小于效应

  • 程序在遇到一个变量时,使用时作用域查找顺序,不同层次的函数内都有可能定义相同名字的变量,一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变量定义会按照顺序从本层往外依次查找,直到第一个变量定义。整个过程中会发生内层变量的效果,叫做“遮蔽效应”
/* 
1、只要是代码就至少有一个作用域
2、写在函数内部的局部作用域
3、如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
4、根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,
就被称作作用域链
*/

// 作用域链 : 内部函数访问外部函数的变量 采取的是链式查找的方式来决定取哪个值 这种结构我们称作用域链
// 就近原则
var num = 10;

function fn() {//外部函数
var num = 20;

function fun() {//内部函数
console.log(num);//20
}

fun();

}
fn();

不写 var 关键字的影响

  • 在函数内部想要定义新的变量,如果不使用关键字 var ,相当于定义的全局变量。如果全局变量也有相同的标识符,会被函数内部的变量影响,局部变量污染全局变量
  • 注意:每次定义变量时都必须写 var 关键字,否则就会定义在全局,可能污染全局
function fn() {
a = 2;
}
console.log(a);//2


预解析

  • JavaScript 代码的执行是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器执行 JavaScript 代码的时候,分为两个过程:预解析过程和代码执行过程
  • 预解析过程:
  1. 把变量的声明提升到当前作用域的最前面,只会提升声明,不会提升赋值
  2. 把函数的声明提升到当前作用域的最前面,只会提升声明,不会提升调用
  3. 先提升 var ,再提升 function
  • Javascript 的执行过程:在预解析之后,根据新的代码顺序,从上往下按照既定规律执行 js 代码

变量声明提升

  • 在与解析过程中,所有定义的变量,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的声明变量过程
  • 提升过程中,只提升声明过程,不提升变量赋值,相当于变量定义未赋值,变量内存储 undefined 值
  • 因此,在 js 中会出现一种现象,在前面调用后定义的变量,不会报错,只会使用 undefined值

函数声明提升

  • 在与解析过程中,所有定义的函数,都会将声明的过程提升到所在的作用域最上面,在将来的代码执行过程中,按照先后顺序会先执行被提升的函数声明过程
  • 在预解析之后的代码执行过程中,函数定义过程已经在最开始就会执行,一旦函数定义成功,后续就可以直接调用函数
  • 因此,在 js 中会出现一种特殊现象,在前面调用后定义的函数,不会报错,而且能正常执行函数内部的代码(如果使用 var 声明的函数,在定义函数之前调用函数,会直接报错
/*
1、
console.log(num);报错
*/

// 2、
console.log(num);//undefined
var num = 10;

// 3、
fn();//11

function fn() {
console.log(11);
}
// 4、
/*
fun();//报错
var fun = function() {
console.log(22);
}
*/

/*
1、我们js引擎运行js 分为两步: 预解析 代码执行
(1) 预解析 js引擎会把js 里面所有的 var 和 function 提升到当前作用域的最前面
(2) 代码执行 按照代码书写的顺序从上往下执行

2、预解析分为 变量预解析(变量提升) 函数与解析(函数提升)
(1) 变量提升 就是把所有的变量声明提升到当前的作用域最前面 不提升赋值操作
(2) 函数提升 把所有的函数声明提升到当前作用域的最前边 不调用函数
*/

提升顺序

  • 预解析过程中,先提升 var 变量声明,在提升 function 函数声明
  • 假设出现变量名和函数名相同,那么后提升的函数名标识符会覆盖先提升的变量名,那么在后续代码种出现调用标识符时,内部是函数的定义过程,而不是 undefined
  • 如果调用标识符的过程在源代码函数和变量定义的后面,相当于函数名覆盖了一次变量名,结果在执行到变量赋值时,又被新值覆盖了函数的值,那么在后面再次调用标识符,用的就是变量存的新值
  • 建议:不要书写相同的标识符给变量名或函数名,避免出现覆盖

函数声明提升的应用

  • 函数声明提升可以用于调整代码的顺序,将大段的定义过程放到代码最后,但是不影响代码执行效果

IIFE 自调用函数

  • IIFE:immediately-invoked function expression,叫做即时调用的函数表达式,也叫做自调用函数表达式,表示函数在自定义时就立即调用
  • 函数调用方式:函数名或函数表达式的变量名后面加 () 运算符
  • 函数名定义的形式不能实现立即执行自调用,函数使用函数表达式形式可以实现立即执行,原因是因为函数表达式定义过程中,将函数矮化成表达式,后面加 () 运算符就可以立即执行
  • 启发:如果想实现 IIFE ,可以想办法将函数矮化成表达式
// 关键字定义的方式,不能立即执行
// function fun() {
// console.log(1);
// }();

// 函数表达式,可以立即调用
var foo = function () {
console.log(2);
}();
  • 函数矮化成表达式,就可以实现自调用
  • 函数矮化成表达式的方法,可以让函数参与一些运算,也就是说给函数前面加一些运算符。

数学运算符:+ - ()

逻辑运算符:!非运算

  • IIFE 结构可以封住函数的作用域,在结构外面是不能调用函数的
  • IIFE 最常用的时 () 运算符,而且函数可以不写函数名,使用匿名函数
// 通过前面添加操作符可以将我们的函数矮化成表达式
+ function fun() {
console.log(1);
}();
- function fun() {
console.log(1);
}();
(function fun() {
console.log(1);
})();
!function fun() {
console.log(1);
}();



收起阅读 »

JavaScript 数组

为什么学习数组?之前学习的数据类型,只能存储一个值(比如:Number/String)。如果我们想存储班级中所有学生的成绩,此时该如何存储?数组的概念所谓数组(Array),就是将多个元素(通常是同一类型)按一定顺序排列放到一个集合中,那么这个集合我们就称之为...
继续阅读 »
为什么学习数组?
  • 之前学习的数据类型,只能存储一个值(比如:Number/String)。如果我们想存储班级中所有学生的成绩,此时该如何存储?

数组的概念

  • 所谓数组(Array),就是将多个元素(通常是同一类型)按一定顺序排列放到一个集合中,那么这个集合我们就称之为数组

数组的定义

  • 数组是一组有序的数组集合。数组内部可以存放多个数据,不限制数据类型,并且数组的长度可以动态的调整。
  • 创建数组最简单的方式就是数组字面量方式
  • 数组的字面量:[]
  • 一般将数组字面量赋值给一个变量,方便后期对数组进行操作
  • 如果存放多个数据,每个数据之间用逗号分隔,最后一个后面不需要加逗号
var arr = [];//创建一个空的数组
var arr1 = [1, 2, '数组', true, undefined, true];

获取数组元素

  • 数组可以通过一个 index (索引值、下标)去获取对应的某一项数据,进行下一步操作
  • index:从 0 开始,按照整数排序往后顺序排序,例如 0,1,2,3······
  • 可以通过 index 获取某项值之后,使用或者更改数组项的值
  • 调用数据:利用数组变量名后面直接加 [index] 方式
var arr = ['red', 'orange', 'blue',];//索引号按顺序0 1 2...
console.log(arr[0]); //red
console.log(arr[1]); //orange
console.log(arr[2]); //blue
console.log(arr[3]); //undefined
// 从代码中我们可以发现,从数组中取出每一个元素时,代码是重复的,不一样的是代码的索引值在增加
// 因此我们有更简便的方法一次调用数组中的多个元素
  • 注意:如果索引值超过了数组最大项,相当于这一项没有赋值,内部存储的就是 undefined
  • 更改数据:arr[index] 调用这一项数据,后面等号赋值更改数据
var arr = [1, 2, '数组', true];
console.log(arr[5]);//undefined
arr[2] = 'haha';
console.log(arr[2]);//'haha'


数组的长度

  • 数组有一个 length 的属性,记录的是数组的数据的总长度
  • 使用方法:变量.length
console.log(arr.length);
  • 数组的长度与数组最后一项的下标存在关系,最后一项的下标等于数组的 length-1
  • 获取最后一项数据时,可以这样书写:
console.log(arr[arr.length-1]);
  • 数组的长度不是固定不变的,可以发生更改

更改数组长度:

  • 增加数组长度:直接给数组 length 属性赋一个大于原来长度的值。赋值方式使用等号赋值
  • 或者,可以给一个大于最大下标的项直接赋值,可以强制拉长数组
  • 缩短数组长度:强制给 length 属性赋值,后面数组被会直接删除,删除时不可逆的

更改数组长度:

var arr = [1, 3, 5, 7];
arr.length = 10;
console.log(arr);

拉长数组长度:

var arr = [1, 3, 5, 7];
arr[14] = 6;
console.log(arr);
console.log(arr.length);//15

缩短数组长度:

var arr = [1, 3, 5, 7];
arr.length = 3
console.log(arr);//[1,3,4]
console.log(arr.length);//3


数组的遍历

  • 遍历:遍及所有,对数组的每一个元素都访问一次就叫遍历。利用 for 循环,将数组中的每一项单独拿出来,进行一些操作
  • 根据下标在 0 到 arr.length-1 之间,进行 for 循环遍历
//遍历数组就是把数组的元素从头到尾访问一遍
var arry = ['red','blue','green']
for(var i = 0; i < 3; i++){
console.log(arry[i]);
}
//1.因为索引号从0开始,所以计数器i必须从0开始
//2.输出时计数器i当索引号使用

// 通用写法
var arry = ['red','blue','green']
for(var i = 0; i < arry.length; i++){ //也可以写成: i <= arry.length - 1
console.log(arry[i]);
}


数组应用案例

  • 求一组数中的所有数的和以及平均值
var arry = [2, 6, 7, 9, 11];
var sum = 0;
var average = 0;
for (var i = 0; i < arry.length; i++) {
sum += arry[i];
}
average = sum / arry.length;
console.log(sum,average);//同时输出多个变量用逗号隔开

原文:https://zhuanlan.zhihu.com/p/365784347

收起阅读 »

JavaScript 常见的三种数组排序方式

一、冒泡排序冒泡排序 的英文名是 Bubble Sort ,它是一种比较简单直观的排序算法简单来说它会重复走访过要排序的数列,一次比较两个数,如果他们的顺序错误就会将他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成这个算法...
继续阅读 »

一、冒泡排序

冒泡排序 的英文名是 Bubble Sort ,它是一种比较简单直观的排序算法

简单来说它会重复走访过要排序的数列,一次比较两个数,如果他们的顺序错误就会将他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成

这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端


算法思路(以按递增顺序排列为例):

1、我们需要做一个内层循环来比较每对相邻元素的大小,如果前面大于后面,就让他们交换位置,我们要让小的数在前面,大的数在后面

2、当内层循环结束时,在数组最后一位的元素,就一定是这个数组中最大的元素了,这时候除了最后一个元素不用再动以外(所以内层循环每循环一次就可以少循环一次)我们还要再来确定这个数组中第二大的元素,第三大的元素,以此类推,因此我们还需要一层外层循环。如果这个数组有 n 个元素我们就要确定 n - 1 个元素的位置,所以外层循环需要循环的次数就是 n - 1 次

3、只需要内外两层循环嵌套,就可以把数组排序好啦,虽然实现方式可能有很多种,这只是我个人的想法,代码如下,排序功能已封装成函数,请放心食用:

var myArr = [89,34,76,15,98,25,67];

function bubbleSort(arr) {
for (var i = 0; i < arr.length - 1; i++) {
for (var j = 0; j < arr.length - i; j++) {
if(arr[j] > arr[j + 1]) {
// 交换两个数的位置
var temp = 0;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}

return arr;
}

console.log(bubbleSort(myArr));

二、选择排序

选择排序 英文叫法是 Selection sort,这也是一种简单直观的排序方法

这种排序首先会在未排序的数组中找到最小或者最大的元素,存放在排序数组的起始位置

然后再从未排序的数列中去找到这个数组中第二大或这第二小的数放在已排序的数之后,以此类推,不断重复直到所有元素排列完毕


算法思路(以按递增顺序排列为例):

1、我们需要内层循环找出未排序数列中的最小值(找最小值可以用之前谁比最小值小谁就替换最小值的思路),循环后找到未排序数列中的最小元素时记录最小的那个元素在数组中的索引值,用索引获得最小值的位置后把它放在数组的第一位,此处注意,如果直接放在第一位会替换第一位数组中原来的元素,我们需要交换最小值的位置,和第一个元素的位置(利用两个变量交换数值的方法)

2、每经过一次内层循环,我们就能确定一个未排序数组中最小值的位置,在确定倒数第二个数的位置时,最后一个数的位置也自然而然地被确定了,因此数组中有 n 个元素我们就需要进行 n - 1 次内层循环,我们就用用外层循环来保持内层循环的重复进行

var myArr = [89,34,76,15,98,25,67];

function selectSort(arr) {
for(var i = 0; i < arr.length - 1; i++) {
//i < arr.length - 1 因为排完倒数第二个,倒数第一个数自然在它正确的位置了
var index = i;
for(var j = i + 1; j < arr.length; j++) {
// 寻找最小值
if(arr[index] > arr[j]){
// 保存最小值索引
index = j;
}
}

// 将未排序中的最小数字,放到未排序中的最左侧
if(index != i) {
var temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
}
return arr;
}

console.log(selectSort(myArr));


三、插入排序

插入排序 英文称为 Insertion sort ,插入排序也被叫做直接插入排序

它的基本思想是将一个未排序的元素插入到已经排序好的数组中,从而使得已排序的数组增加一个元素,通过插入不断完善已排序数组的过程,就是排序整个数组的过程。


算法思路(以按递增顺序排列为例):

1、因为数组中第一个元素前面没有元素可以进行比较,所以我们从第二个元素开始比较,用 current 变量来进行存储当前要和别人比较的元素,用 preIndex变量 来方便我们去找当前准备插入元素之前的元素

2、内层循环就是按顺序比较插入元素和之前元素的大小,来确定插入元素的位置, preIndex 每比较一次就自减1 ,让准备插入元素和它之前的所有已排序元素都比较一遍,每当待插入元素比前一个数小了,前面的元素就往右挪一个位置,直到前一个数小于待插入数,跳出判断,待插入元素放在前一次判断挪动元素留出的空位上,由于我们提前用 current 保存了要插入的元素,所以要插入的元素不会因为前面的元素覆盖而丢失。

3、每循环一次内层循环,我们就可以确定一个元素的插入位置,但由于我们内层循环是从第二个元素开始的(也就是索引为 1 的元素),因此如果有 n 个元素,我们就需要 n - 1 次内层循环,内存循环我们用外层循环来实现,外层循环就这么被定义完成了

var myArr = [89,34,76,15,98,25,67];

function insertionSort(arr) {
for (var i = 1; i <= arr.length - 1; i++) {
var preIndex = i - 1;
current = arr[i];
while(preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}

console.log(insertionSort(myArr));


总结:

选择排序(一种不稳定的排序方法)

优点:移动数据的次数已知(n-1次);

缺点:比较次数多。


冒泡排序

优点:稳定;

缺点:慢,每次只能移动相邻两个数据。


插入排序

优点:稳定,快;

缺点:比较次数不一定,比较次数越少,插入点后的数据移动越多,特别是当数据总量庞大的时候,但用链表可以解决这个问题。

原文:https://zhuanlan.zhihu.com/p/368208410

收起阅读 »

js 取小数点后几位方法

一 取后两位 为例: 四舍五入 1.toFixed() Number的toFixed()方法可把 Number 四舍五入为指定小数位数的数字。 const test = 1.12 / 3 // 0.37333333333333335 console.lo...
继续阅读 »

一 取后两位 为例:


四舍五入


1.toFixed()

Number的toFixed()方法可把 Number 四舍五入为指定小数位数的数字。



const test = 1.12 / 3 // 0.37333333333333335
console.log(test.toFixed(2)) // 0.37
复制代码

注意:
.兼容问题



/**
* firefox/chrome ie某些版本中,对于小数最后一位为 5 时进位不正确(不进位)。
* 修复方式即判断最后一位为 5 的,改成 6, 再调用 toFixed
*/
function(number, precision) {
const str = number + '';
const len = str.length;
let last = str[len - 1] // 或者 str.substr(len - 1, len);
if(last == '5') {
let = '6';
str = str.substr(0, len - 1) + last;
return (str - 0).toFixed(precision)
} else {
return number.toFixed(precision)
}
}

或者为:
function toFixed(number, precision) {
const tempCount = Math.pow(10, precision);
let target = number * tempCount + 0.5;
target = parseInt(des, 10) / tempCount;
return target + '';
}

复制代码

.精确问题



/**
* toFixed 有时候会碰到如下精度缺失问题
* 可以使用下面例子的方法解决
* 或者 (test * 100).toFixed(2) + '%';
*/
const test = 1.12 / 3 // 0.37333333333333335
console.log(test.toFixed(4)) // 0.3733
console.log((test).toFixed(4) * 100 + '%') // 37.330000000000005%

复制代码


  1. Math.round()



/**
* 利用Math.round
* 保留两位小数
*/
function toDecimal(num) {
let tar = parseFloat(num);
if (isNaN(tar)) { return };
tar = Math.round(num * 100) / 100;
}


/**
* 利用Math.round 强制保留两位小数 10 则为 10.00
* 保留两位小数
*/

function toDecimal(num) {
let tar = parseFloat(num);
if (isNaN(tar)) {return};
tar = Math.round(num * 100) / 100;

let tarStr = tar.toString();
let decIndexOf = tarStr.indexOf('.');
if(decIndexOf < 0) {
tarStr += '.';
decInexOf = tarStr.length;
}
while (tarStr.length <= decIndexOf + 2) {
tarStr += '0';
}
return tarStr;
}

复制代码

不四舍五入


1.先把小数取整 在计算



const test = 1.12 / 3 // 0.37333333333333335
Math.floor(test * 100) / 100 // floor 是向下取整 0.37
复制代码

2.使用正则表达式



const test = 1.12 / 3 // 0.37333333333333335
let target = test + '' // test.toString()
target = target.match(/^\d+(?:\.\d{0, 2}?/)
//输出结果为 0.37。但整数如 10 必须写为10.0000
// 如果是负数,先转换为正数再计算,最后转回负数


作者:maomaoweiw
链接:https://juejin.cn/post/6844903638020816903

收起阅读 »

JavaScript的小技巧

类型转换数组转字符串var arr = [1,2,3,4,5]; var str = arr+''; //1,2,3,4,5 字符串转数字var str = '777'; var num = str * 1; //777 var str = '777'; v...
继续阅读 »

类型转换

数组转字符串

var arr = [1,2,3,4,5];
var str = arr+''; //1,2,3,4,5

字符串转数字

var str = '777';
var num = str * 1; //777

var str = '777';
var num = str - 0; //777

字符串转数字

var str = '666';
var num = str * 1; // 666

向下取整

var num = ~~4.2144235; //  4

var num = 293.9457352 >> 0; // 293

boolean 转换

var bool = !!null; //  false
var bool = !!'null'; // true

var bool = !!undefined; // false
var bool = !!'undefined'; // true

var bool = !!0; // false
var bool = !!'0'; // true

var bool=!!''; // true
var bool=!![]; // true
var bool=!!{}; // true

var bool=!!new Boolean('false'); // true
var bool=!!new Boolean('true'); // true

判断对象下面是否有此属性

直接判断

var obj = {a:789};
if(obj.a){ //obj.b ==>789
console.log('运行了') //可以运行
}

if(obj.b){ //obj.b ==>undefined
console.log('运行了') //没有运行
}

var obj2 = {a:false};
if(obj2.a){ //obj.b ==>false
console.log('运行了') //没有运行
}
// 不严谨,如果值为0,undefined,false,null... 也会判断为false

in 操作符

var obj = {a:789};
if('a' in obj){ // 'a' in obj ==>true
console.log('运行了') //可以运行
}

if('b' in obj){// 'a' in obj ==>false
console.log('运行了') //没有运行
}

利用hasOwnProperty

var obj = {a:789};
if(obj.hasOwnProperty('a')){ //==>true
console.log('运行了') //可以运行
}

if(obj.hasOwnProperty('b')){ //==>false
console.log('运行了') //没有运行
}

还有好多好多,得慢慢写

原文:https://zhuanlan.zhihu.com/p/368353172

收起阅读 »

uniapp实现$router

作为 Vue 重度用户,在使用 uni-app 过程中不可避免的把 Vue 开发习惯带了过去。无论是项目目录结构,还是命名风格,甚至我还封装了一些库,如 https://zhuanlan.zhihu.com/p/141451626 提到的 ...
继续阅读 »

作为 Vue 重度用户,在使用 uni-app 过程中不可避免的把 Vue 开发习惯带了过去。无论是项目目录结构,还是命名风格,甚至我还封装了一些库,如 https://zhuanlan.zhihu.com/p/141451626 提到的 _request 等。

众所周知,用 Vue 开发项目,其实就是用的 Vue 全家桶。即 Vue + Vuex + VueRouter 。在代码里的体现就是:

this + this.$store + this.$router/$route

然而由于 uni-app 为了保证跨端同时简化语法,用的是微信小程序那套 API。其中就包括路由系统。因为在 uni-app 中,没有 $router/$route。只有 uni[‘路由方法’]。讲真的,这样做确实很容易上手,但同时也是有许多问题:

  1. 路由传参数只支持字符串,对象参数需要手动JSON序列化
  2. 传参有长度限制
  3. 传参不支持特殊符号如 url
  4. 不支持路由拦截和监听

因此,需要一个工具来将现有的路由使用方式变为 vue-router 的语法,并且完美解决以上几个问题。

vue-router 的语法这里不再赘述。简单的来说就是将路由的用法由:

uni.navigateTo({
url: `../login/login?data=${JSON.stringify({ from: 'index', time:Date.now() })}`
})

变成:

this.$router.push('/login', {
data: {
from: 'index',
time: Date.now()
}
})

同时传参通过一个 $route 对象。因此我们的需求就是事现一个 $router 和 $route 对象。并给定相应方法。比如调用:

push('/login')

其实就是执行了:

uni.navigateTo({ url:`../login/login ` })

实现起来非常简单:

push 方法接收到 '/login' 将其拼接为 `../login/login` 后调用 uni.navigateTo 就可以。

然而这样并不严谨。此时的 push 方法只能在页面内使用。而不能在 pages 文件夹以外的地方使用,因为这里用的是相对路径。只要改成 `pages/login/login` 就好。

$route 的实现就是在路由发生变化时,动态改变一个公共对象 route 的内部值。

而通过全局 mixin onShow 方法,可以实现对路由变化动态监听。

通过 require.context 预引入路由列表实现更好的错误提示。

最后通过一个页面堆栈数据列表实现 route 实时更新。

最后的代码:

import Vue from 'vue'

export const route = { // 当前路由对象所在的 path 等信息。默认为首页
fullPath: '/pages/index/index',
path: '/index',
type: 'push',
query: {}
}

let onchange = () => {} // 路由变化监听函数
const _$UNI_ACTIVED_PAGE_ROUTES = [] // 页面数据缓存
let _$UNI_ROUTER_PUSH_POP_FUN = () => {} // pushPop resolve 函数
const _c = obj => JSON.parse(JSON.stringify(obj)) // 简易克隆方法
const modulesFiles = require.context('@/pages', true, /\.vue$/) // pages 文件夹下所有的 .vue 文件

Vue.mixin({
onShow() {
const pages = getCurrentPages().map(e => `/${e.route}`).reverse() // 获取页面栈
if (pages[0]) { // 当页面栈不为空时执行
let old = _c(route)
const back = pages[0] != route.fullPath
const now = _$UNI_ACTIVED_PAGE_ROUTES.find(e => e.fullPath == pages[0]) // 如果路由没有被缓存就缓存
now ? Object.assign(route, now) : _$UNI_ACTIVED_PAGE_ROUTES.push(_c(route)) // 已缓存就用已缓存的更新 route 对象
_$UNI_ACTIVED_PAGE_ROUTES.splice(pages.length, _$UNI_ACTIVED_PAGE_ROUTES.length) // 最后清除无效缓存
if (back) { // 当当前路由与 route 对象不符时,表示路由发生返回
onchange(route, old)
}
}
}
})

const router = new Proxy({
route: route, // 当前路由对象所在的 path 等信息,
afterEach: to => {}, // 全局后置守卫
beforeEach: (to, next) => next(), // 全局前置守卫
routes: modulesFiles.keys().map(e => e = e.replace(/^\./, '/pages')), // 路由表
_getFullPath(route) { // 根据传进来的路由名称获取完整的路由名称
return new Promise((resolve, reject) => {
const fullPath = this.routes.find(e => RegExp(route + '.vue').test(e))
fullPath ? resolve(fullPath.replace(/\.vue$/, '')) : reject(`路由 ${ route + '.vue' } 不存在于 pages 目录中`)
})
},
_formatData(query) { // 序列化路由传参
let queryString = '?'
Object.keys(query).forEach(e => {
if (typeof query[e] === 'object') {
queryString += `${e}=${JSON.stringify(query[e])}&`
} else {
queryString += `${e}=${query[e]}&`
}
})
return queryString.length === 1 ? '' : queryString.replace(/&$/, '')
},
_beforeEach(path, fullPath, query, type) { // 处理全局前置守卫
return new Promise(resolve => {
this.beforeEach({ path, fullPath, query, type }, resolve)
})
},
_next(next) { // 处理全局前置守卫 next 函数传经来的方法
return new Promise((resolve, reject) => {
if (typeof next === 'function') { // 当 next 为函数时, 表示重定向路由,
reject('在全局前置守卫 next 中重定向路由')
Promise.resolve().then(() => next(this)) // 此处一个微任务的延迟是为了先触发重定向的reject
} else if (next === false) { // 当 next 为 false 时, 表示取消路由
reject('在全局前置守卫 next 中取消路由')
} else {
resolve()
}
})
},
_routeTo(UNIAPI, type, path, query, notBeforeEach, notAfterEach) {
return new Promise((resolve, reject) => {
this._getFullPath(path).then((fullPath) => { // 检查路由是否存在于 pages 中
const routeTo = url => { // 执行路由
const temp = _c(route) // 将 route 缓存起来
Object.assign(route, { path, fullPath, query, type }) // 在路由开始执行前就将 query 放入 route, 防止少数情况出项的 onLoad 执行时,query 还没有合并
UNIAPI({ url }).then(([err]) => {
if (err) { // 路由未在 pages.json 中注册
Object.assign(route, temp) // 如果路由跳转失败,就将 route 恢复
reject(err)
return
} else { // 跳转成功, 将路由信息赋值给 route
resolve(route) // 将更新后的路由对象 resolve 出去
onchange({ path, fullPath, query, type }, temp)
!notAfterEach && this.afterEach(route) // 如果没有禁止全局后置守卫拦截时, 执行全局后置守卫拦截
}
})
}
if (notBeforeEach) { // notBeforeEach 当不需要被全局前置守卫拦截时
routeTo(`${fullPath}${this._formatData(query)}`)
} else {
this._beforeEach(path, fullPath, query, type).then((next) => { // 执行全局前置守卫,并将参数传入
this._next(next).then(() => { // 在全局前置守卫 next 没传参
routeTo(`${fullPath}${this._formatData(query)}`)
}).catch(e => reject(e)) // 在全局前置守卫 next 中取消或重定向路由
})
}
}).catch(e => reject(e)) // 路由不存在于 pages 中, reject
})
},
pop(data) {
if (typeof data === 'object') {
_$UNI_ROUTER_PUSH_POP_FUN(data)
}
uni.navigateBack({ delta: typeof data === 'number' ? data : 1 })
},
// path 路由名 // query 路由传参 // isBeforeEach 是否要被全局前置守卫拦截 // isAfterEach 是否要被全局后置守卫拦截
push(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.navigateTo, 'push', path, query, notBeforeEach, notAfterEach)
},
pushPop(path, query = {}, notBeforeEach, notAfterEach) {
return new Promise(resolve => {
_$UNI_ROUTER_PUSH_POP_FUN(null)
_$UNI_ROUTER_PUSH_POP_FUN = resolve
this._routeTo(uni.navigateTo, 'pushPop', path, query, notBeforeEach, notAfterEach)
})
},
replace(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.redirectTo, 'replace', path, query, notBeforeEach, notAfterEach)
},
switchTab(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.switchTab, 'switchTab', path, query, notBeforeEach, notAfterEach)
},
reLaunch(path, query = {}, notBeforeEach, notAfterEach) {
return this._routeTo(uni.reLaunch, 'reLaunch', path, query, notBeforeEach, notAfterEach)
}
}, {
set(target, key, value) {
if (key == 'onchange') {
onchange = value
}
return Reflect.set(target, key, value)
}
})

Object.setPrototypeOf(route, router) // 让 route 继承 router
export default router


收起阅读 »

uniapp与flutter,跨平台解决方案你该如何选择

为了做毕设,用了下uniapp与flutter,说真的,这是两款十分优秀的产品,几乎做到了各自领域性能和跨平台的极致。那么这两款产品到底有什么不同,在选型的时候应该如何取舍,这是我写这篇文章的目的。uniapp与flutter都是为了解决跨平台问题的框架uni...
继续阅读 »

为了做毕设,用了下uniapp与flutter,说真的,这是两款十分优秀的产品,几乎做到了各自领域性能和跨平台的极致。那么这两款产品到底有什么不同,在选型的时候应该如何取舍,这是我写这篇文章的目的。

uniapp与flutter都是为了解决跨平台问题的框架

uniapp是从h5 app到小程序一步步发展过来的,也就是走的html的路线。

html从最早的网页套壳一步步发展至今,为了解决早期套壳的体验问题,我们尝试用js代码调用原生接口,与原生进行交互,出现了一系列如React Native,Cordova,Weex,Framework7,MUI之类的框架,这些框架的出现进一步丰富h5应用的功能。但是这些技术要求很高的优化技巧,要走很多坑,在ios的体验尚可,但是Android上由于更新维护问题,js引擎差别很大,早期Android的js引擎极差,这些框架使用体验都不好,当然也有硬件方面的原因。而且Android上webview存在性能瓶颈,复杂应用不做预加载的情况下使用体验真的不好。后来为了使体验达到h5所能做的极致,小程序出现了,为了性能,屏蔽了dom,规定了独特的规范,按照这些规范去写,编译时框架提前给你优化好,事实证明这样做确实可以提高h5应用的使用体验。

uniapp延续了小程序的思路,和vue结合,屏蔽dom,提前优化,确实很好,也做到了跨平台,这是一款极为优秀的跨各种小程序的解决方案,与它自家的h5+结合也是一个还算不错的h5+ app的前端框架。但是uniapp的定位中有一个极大的问题,就是小程序与h5 app之间的距离太大了,强跨的体验真是极差,得不偿失。举个栗子,3d渲染,多人视频,nfc写卡,这种小程序完全做不到,当然uniapp也可以调h5+ runtime,但是一个复杂的移动端应用可能会加各种各样的东西,你完全预料不到可能出现什么需求,并且这些需求越来越多的情况下,小程序端与移动端分开维护是必然的结果,强行结合只能是结构混乱,难以维护。那么如果分开维护,uniapp与前面提到的那些框架并没有明显优势。

那么接着说flutter,flutter与h5技术栈的思路完全不同,JSCore,V8再怎样优秀,也始终解决不了JavaScript本身语法缺陷和运行在浏览器的事实。

===========================

这里我之前写flutter用dart做了一个渲染引擎,有人言辞激烈的抨击了我的错误,后来我仔细看了一下资料。


官网上是这样说的

Flutter is built with C, C++, Dart, and Skia (a 2D rendering engine).See this architecture diagram for abetter picture of the main components.

确实,dart只是用来组织各种控件的一个工具,这个图形渲染是用了这个叫Skia的图形库

Skia is an open source 2D graphics library which provides common APIs that work across a variety of hardware and software platforms.

这个Skia,Google旗下,开源2D图形库,提供了多种软硬件平台的通用API。

确实是我的错,没调查清楚,但是这个方式还是令我觉得,很难受。

=================================

也就是说flutter和cocos,unity3d一样,完全可以用来写游戏,突破60fps,而且自己渲染,大大减少了与原生的通信次数,并且使用 Platform Channels 来跟系统通信大大丰富了一些偏门功能的应用,去组件库看了下tcp直连mqtt都支持了,刚好毕设会用到,开心。

所以如果你需要跨平台,技术选型时遇到问题

1.看需求

如果你的应用需求足够简单,像小程序之类的完全可以做到,选uniapp。因为说真的,像点单这种功能,谁没事愿意专门装个肯打鸡,coco之类在手机上,反正我去点单的时候,能用小程序我就不会再装app了,如果有人愿意装app,稍微改改顺便出个app版看着比较好看。

如果你的需求复杂,必然要分开维护,还是和之前一样,uniapp是一个极好的跨各种小程序的解决方案,一次编译,微信小程序,支付宝小程序,百度小程序,多端运行。那app端你可以再选择h5或者flutter。

如果你需要适配横屏,建议用flutter,横屏的交互加上material design的加持,这样和桌面端就没有太大区别了,目前flutter已经可以编译运行在Windows和linux上,虽然目前还很不完善,但是Google的野心和背书能力让我觉得flutter的野心不止于此。未来能附赠一套桌面端,意外之喜。


2.学习成本

flutter的学习成本主要在Dart,而uniapp主要在vue。说真的,我之前做Android和JavaWeb的,Java转Dart真的没有压力,有人说flutter嵌套太多,安卓xml布局嵌套不多吗,公司现在维护的ERP系统jQuery写的跟使一样,各种+ " append。

而我一个传统Java使用者刚开始遇到vue真的难受了好一阵子,这个this的真是vue里令我最难受的,使一样。推荐周围同学学uniapp,学过C++,Java的普遍反映也是vue看不懂。你们再也不是像jQuery一样好单纯好不做作的前端了。

总之前端的uniapp学习成本低,学过后端Java,C++的,flutter上手成本低。


3.社区

刚开始Google要出Fuchsia OS的时候我还嗤之以鼻,真当程序狗们都会乖乖听你话吗,那win phone坟头草都老高了。没想到啊,你们早在苹果骂安卓垃圾的时候就想着今天了吧。

Google在安卓界的背书能力感觉跟Spring在JavaWeb界的背书能力不逞多让,只要Android和Fuchsia不死,Flutter应该不会有太大问题,而且Flutter的社区是真的真的真的很活跃啊,github上问题的解决速度和出视频的速度真是令我叹为观止。

相比之下DCloud出MUI到现在不愠不火就让我不禁对uniapp有些担心,虽然微信,支付宝在后面背书,希望一群国内一线大厂们能给力点吧。而且我在uniapp提的问题一个多月了,无人问津

【报Bug】使用小程序组件,当参数为函数时,传不过去 - DCloud问答

希望你们珍惜你们的银牌赞助者。而且出视频的速度一言难尽,看B站居然没有,讲道理一个好的教学视频真的很重要,干啃API在学习时真是费力不讨好的事情,你学习的思路和文档的思路是不一样的。不过uniapp的QQ群倒是很火,不管怎样,一个国产的优秀产品,希望你们能有一个好的未来。

原文:https://zhuanlan.zhihu.com/p/55466963

收起阅读 »

uni-app 的使用体验总结

[实践] uni-app 的使用总结最近使用 uni-app 的感受。使用体验没用之前以为真和 Vue 一样,用了之后才知道。有点类似 Vue 和 小程序结合的感觉。写类似小程序的标签,有着小程序和 Vue 的生命周期钩子。对比 uni-app 文档和微信小程...
继续阅读 »

[实践] uni-app 的使用总结


最近使用 uni-app 的感受。

使用体验

没用之前以为真和 Vue 一样,用了之后才知道。有点类似 Vue 和 小程序结合的感觉。写类似小程序的标签,有着小程序和 Vue 的生命周期钩子。对比 uni-app 文档和微信小程序的文档,不差多少,只是将 wx => uni,熟悉 Vue 和 小程序可以直接上手。

如果看过其他小程序的文档,可以发现,文档主要的三大章节就体现在框架组件API 。

uni-app 需要注意看注意事项,文档给出了和 Vue 使用的区别。例如动态的 Class 与 Style 绑定,在 H5 能用,APP 和小程序的体现就不一样。

配置项跟着文档来,开发环境也是现成的,下载 HBuilderX 导入项目就能运行,日常开发习惯了 VSCode,所以 HBuilderX 的主要作用就是用来打包 APK 和起各个端的服务,coding 的话当然还是用 VSCode。

路由

uni-app 的路由全部配置在 pages.json 文件里,就会导致多人开发的时候,路由无法拆分,如果处理的不好,就会发生冲突。

导航

导航栏需要注意的一个问题就是不同端的展示形式会不同,所以要处理兼容问题,导航栏可以自定义,用原生,框架,插件但是兼容性都不同,多端需求一定要在不同设备跑一下看效果。

例如在小程序和 APP 中,原生导航栏取消不了,就不能用自定义的导航栏,要在 pages.json 中配置原生导航栏。

兼容方法就是用 uni-app 提供的条件编译,处理各端不同的差异,我们支付的业务逻辑也是通过条件编译,区分不同端调用不同的支付方式。

生命周期

分为 应用的生命周期页面的生命周期组件的生命周期。写过小程序和 Vue 的很好理解,大致上和 Vue 的还是差不多的,页面生命周期针对当前的页面,应用生命周期针对小程序、APP。这些过程可能都要踩一下!

网络请求和环境配置

官方的 uni.request 虽然封装好了基本的请求,但是没有拦截,我们开始也是自己在这基础上加了层壳,简单的封装发送请求。当然也可以选择第三方库的使用,如 flyio、axios。

我们是前端自己封装了 HTTP 请求,并且统一接口的请求方式,所有的接口放到 api.js 文件中进行统一管理。这样大家在页面请求接口的时候风格才统一,包括约定好请求拦截和响应拦截,具体拦截的参数和后台约定好。

资源优化

  • 暂时接触不到 Webpack 之类的资源打包优化,但是文档中有提到资源预取、预加载、treeShaking 只需要在配置文件中设置即可,或者在开发工具勾上。小程序也是勾选自动压缩混淆。
  • 删除没用到文件和图片资源,因为打包的时候是会算进去的,比如 static 目录下的资源文件都会被打包,而且图片资源太大也不好。
  • uni-app 运行时的框架主库 chunk-vendors.js 文件是经过处理的,部署做 gzip

Web-View 组件

在 uni-app 中使用 Web-View,可以使用本地的资源和网络的资源,不同平台也是有差异的,小程序不支持本地 HTML,且小程序端 Web-View 组件一定有原生导航栏。

需要注意的是网页向应用 postMessage 的时候需要引入 uni.web-view.js,不然是没办法通信拿不到数据。

TODO: 这个坑后面再详细总结下!

全局状态

最开始是直接使用类似小程序的 globalData 来管理我们的全局状态,但是后面发现需求一多,加了各种东西之后,需要取这个状态的时候就很痛苦,做为程序猿嘛,都想偷懒吖,每次都得引入一下 getApp().globalData.data 这样很繁琐可不行,就替换成了 Vuex,需要取这个变量的时候,直接 this.vuex_xxxx 就能拿到这个值。

有段时间重写了 HTTP 请求部分和全局状态管理部分。

小程序中要在每一个页面中添加使用共有的数据,可以有三种方式解决。

Vue.prototype

它的作用是可以挂载到 Vue 的所有实例上,供所有的页面使用。

// main.js
Vue.prototype.$globalVar = "Hello";

然后在 pages/index/index 中使用:

<template>
<view>{{ useGlobalVar }}</view>
</tempalte>
<script>
export default {
data (){
return {
useGlobalVar: $globalVar
}
}
}
</script>

globalData

<!-- App.vue -->
<script>
export default {
globalData:{
data:1
}
onShow() {

getApp().globalData.data; // 使用

getApp().globalData.data = 1; // 更新

};
</script>

Vuex

Vuex 是 Vue 专用的状态管理模式。能够集中管理其数据,并且可观测其数据变化,以及流动。


之前看到一个通俗化比喻:用交通工具来比喻项目中这几种描述全局变量的方式。

下面列举这些方式通俗的理解状态:

Vue 插件 vue-bus 可以来管理一部分全局变量(叫应用状态吧),学习后发现,bus(中文意思:公交车)这名字取得挺形象的。

先罗列一下这些方式,不过这种分类并不严谨。

1、VueBus:公交车 2、Vuex:飞机 3、全局 import

  • a.new Vue():专车;
  • b.Vue.use:快车;
  • c.Vue.prototype:顺风车。

4、globalData:地铁

首先 VueBus,像公交车一样灵活便捷,随时都可以乘坐;表现在代码里,很轻便,召之即来,缺点就是不好维护,没有一个专门的文件去管理这些变量。想象平时等公交车的心情,知道它回来,但不知道它什么时候来,给人一种很不安的感觉。

而 Vuex,它像飞机,很庄重,塔台要协调飞机运作畅顺,飞机随时向地面报告自己的位置,适合用在大型项目。表现代码中,就是集中式管理所有状态,并且以可预测的方式发生变化。也对应着飞机绝对不能失联的特点。

第三种方式是全局 import,分三种类型,分别是:new Vue()Vue.use()Vue.prototype。可以用网约车来比喻,三种类型分别对应:专车、快车、顺风车。都足够灵活,表现在代码里:一处导入,处处可用。

再分别说明:

new Vue() 就像滴滴的礼橙专车,官方运营,安全可靠。表现在代码里,就是只有 Vue 官方维护的库才能使用这种方式。

Vue.use() 就像快车,必须符合滴滴的规范,才能成为专职司机。表现在代码中,就是导入的插件(或者库)必须符合 Vue 的写法(即封装了 Vue 插件写法)。

Vue.prototype 像顺风车,要求没上面两个那么严,符合一般 js 写法就行,就像顺风车的准入门槛稍稍低一点。

当然,uni-app 的项目里还有可以用 globalData 定义全局变量,非要比喻,可以用地铁,首先比 vue-bus 更好管理维护,想象地铁是不是比公交更可靠;其次比 Vuex 更简单,因为 globalData 真的就是简单的定义一些变量。

globalData 是微信小程序发明的,Vue 项目好像没有对应的概念,但是在 uni-app 中一样可用。

上面说到,这种分类方式不严谨,主要体现在原理上,并不是简单的并列关系或包含关系。

插件市场

uni-app 的主要特色也源自于它的插件市场十分丰富。

用得比较好的组件:

uView:我们用了这个库的骨架屏。这个库还是有很多技巧可以学到的。

https://www.uviewui.com/js/intro.html

ColorUI-UniApp:是个样式库,不是组件库。

https://ext.dcloud.net.cn/plugin?id=239

答题模版:左右滑答题模版,单选题、多选项,判断题,填空题,问答题。基于 ColorUI 做的。

https://ext.dcloud.net.cn/plugin?id=451

uCharts 高性能跨全端图表:

https://ext.dcloud.net.cn/plugin?id=271

最后:各端的差异性,很多东西,H5 挺好的,上真机就挂了,真机好着的,换小程序就飘了,不同小程序之间也有差异,重点是仔细阅读文档。

云打包限制,云打包(打 APK) 的每天做了限制,超出次数需要购买。

虽然可能一些原生可以实现的功能 uni-app 实现不了,不过整体开发下来还行,很多的坑还是因为多端不兼容,除了写起来麻烦一点,基本上都还是有可以解决的策略。比之前用 Weex 写 APP 开发体验好一点,比 React Native 的编译鸡肋一点(这点体验不是很好),至于 Flutter 还没有试过,有机会的话会试一下。

原文:https://zhuanlan.zhihu.com/p/153500294

收起阅读 »

使用uniapp开发项目来的几点心得体会

先说一下提前须要会的技术要想快速入手uniapp的话,你最好提前学会vue、微信小程序开发,因为它几乎就是这两个东西的结合体,不然,你就只有慢慢研究吧。为什么要选择uniapp???开发多个平台的时候,对,就是开发多端,其中包括安卓、IOS、H5/公众号、微信...
继续阅读 »

先说一下提前须要会的技术

要想快速入手uniapp的话,你最好提前学会vue、微信小程序开发,因为它几乎就是这两个东西的结合体,不然,你就只有慢慢研究吧。

为什么要选择uniapp???

开发多个平台的时候,对,就是开发多端,其中包括安卓、IOS、H5/公众号、微信小程序、百度小程序...等其它小程序时,如果每个平台开发,人力开发成本高,后期维护也难,原生开发周期也长,那Unipp就是你的优先选择,官方是这样介绍的~哈~ 先来说一下uniapp的优点

uniapp优点

优点一,多端支持

当然是多端开发啦,uni-app是一套可以适用多端的开源框架,一套代码可以同时生成ios,Android,H5,微信小程序,支付宝小程序,百度小程序等。

优点二,更新迭代快

用了它的Hbx你就知道,经常会右下角会弹出让你更新,没错,看到它经常更新,这么努力的在先进与优化,还是选良心的了。

优点三,扩张强

你可以把轻松的把uniapp编译到你想要的端,也可以把其它端的转换成uniapp,例如微信小程序,h5等;如果开发app的时候,前端表现不够,你还可以原生嵌套开发。

优点四,开发成本、门槛低

不管你是公司也好,个人也好,如果你想开发多终端兼容的移动端,那uniapp就很适合你,不然以个人的能力要开发多端,哈哈... 洗洗睡觉吧。

优点五,组件丰富

社区还是比较成熟,生态好,组件丰富,支持npm方式安装第三方包,兼容mpvue,DCloud有大量的组件供你使用,当然付费的也不贵,你还可以发布你开发的,赚两个鸡腿钱还是可以的。


开发上的优点暂且不说,大体上的有这么一些,接下来说一下开发过程中的缺点

uniapp缺点

缺点一:爬坑

每个程序前期肯定都会有很多的坑,这里点明一下:腾讯,敢问谁没在微信开发上坑哭过,现在不也爬起来了,2年前有人提的bug,你现在去看,他依然在那,不离不弃呀。uniapp坑也有,一般的都有人解决了,没解决的,你就要慢慢的去琢磨了,官方bug的话,提交反馈,等官方修复。

缺点二:某些组件不成熟

我说的是某些官方组件,像什么地图组件,直播组件等,你要在上面开发一些特别功能的话,那真的是比较费神的。

缺点二:nvue有点蛋疼

某些组件或某些功能,官方明确说,建议用nvue开发,那么问题来了,nvue有很多的局限,特别是css,很多都不支持,什么文字只能是text,只支持class样式,很多的,要看文档来。


暂时从使用上的总结就这么一些,如果你有不同的见解,留言交流交流~~

原文:https://zhuanlan.zhihu.com/p/336773995

收起阅读 »

uni-app 悬浮框动效

<view class="menu" :class="{active:menuFlag}"> <image src="../../static/svg/1.svg" class="menuTrigger" @tap="clickMenu"&...
继续阅读 »


<view class="menu" :class="{active:menuFlag}">
<image src="../../static/svg/1.svg" class="menuTrigger" @tap="clickMenu"></image>
<image src="../../static/svg/2.svg" class="menuItem menuItem1"></image>
<image src="../../static/svg/3.svg" class="menuItem menuItem2"></image>
<image src="../../static/svg/4.svg" class="menuItem menuItem3"></image>
</view>
.menu{
position: fixed;
width: 110rpx;
height: 110rpx;
bottom: 120rpx;
right: 44rpx;
border-radius: 50%;
}
.menuTrigger{
position: absolute;
top: 0;
left: 0;
width: 70rpx;
height: 70rpx;
background-color: green;
border-radius: 50%;
padding: 20rpx;
cursor: pointer;
transition: .35s ease;
}
.menuItem{
position: absolute;
width: 50rpx;
height: 50rpx;
top: 10rpx;
left: 10rpx;
padding: 20rpx;
border-radius: 50%;
background-color: white;
border: none;
box-shadow: 0 0 5rpx 1rpx rgba(0,0,0,.05);
z-index: -1000;
opacity: 0;
}
.menuItem1{
transition: .35s ease;
}
.menuItem2{
transition: .35s ease .1s;
}
.menuItem3{
transition: .35s ease .2s;
}
.menu.active .menuTrigger{
transform: rotateZ(225deg);
background-color: pink;
}
.menu.active .menuItem1{
top: -106rpx;
left: -120rpx;
opacity: 1;
}
.menu.active .menuItem2{
top: 10rpx;
left: -164rpx;
opacity: 1;
}
.menu.active .menuItem3{
top: 126rpx;
left: -120rpx;
opacity: 1;
}
data() {
return {
mask: false,
menuFlag: false,
}
},

clickMenu(){
this.menuFlag = !this.menuFlag;
},


原文链接:https://zhuanlan.zhihu.com/p/364244176

收起阅读 »

async/await 的错误捕获

一、案发现场为了更好的说明,举一个很常见的例子:function getData(data) { return new Promise((resolve, reject) => { if (data === 1) { setTim...
继续阅读 »

一、案发现场

为了更好的说明,举一个很常见的例子:

function getData(data) {
return new Promise((resolve, reject) => {
if (data === 1) {
setTimeout(() => {
resolve('getdata success')
}, 1000)
} else {
setTimeout(() => {
reject('getdata error')
}, 1000)
}
})
}
window.onload = async () => {
let res = await getData(1)
console.log(res) //getdata success
}

这样写可以正常打印getdata success 但是如果我们给getData传入的参数不是1,getData会返回一个reject的Promise,而这个地方我们并没有对这个错误进行捕获,则会在控制台看见这样一个鲜红的报错Uncaught (in promise) getdata error

二、尝试捕获它

1. 踹一脚

捕捉错误,首先想到的就是“踹一脚”:

window.onload = async () => {
try {
let res = await getData(3)
console.log(res)
} catch (error) {
console.log(res) //getdata error
}
}

看似问题已经被解决,但是如果我们有一堆请求,每一个await都需要对应一个trycatch,那就多了很多垃圾代码。或许我们可以用一个trycatch将所有的await包起来,但是这样就很不方便对每一个错误进行对应的处理,还得想办法区分每一个错误。

2. then()

因为返回的是一个Promise,那我们首先想到的就是.then().catch(),于是很快就能写出以下代码:

window.onload = async () => {
let res = await getData(3).then(r=>r).catch(err=>err);
console.log(res) //getdata error
}

这样看起来比“踹一脚”高大上一点了……

三、有没有更好的方式

上面那种方法是有一定问题的,如果getData()返回是resolveres则是我们想要的结果,但是如果getData()返回是rejectres则是err,这样错误和正确的结果混在一起了,显然是不行的。

window.onload = async () => {
let res = await getData(3)
.then((res) => [null, res])
.catch((err) => [err, null])
console.log(res) // ["getdata error",null]
}

这种方式有的类似error first的风格。这样可以将错误和正确返回值进行区分了。但是这种方式会让每一次使用await都需要写很长一段冗余的代码,因此考虑提出来封装成一个工具函数:

function awaitWraper(promise) {
return promise.then((res) => [null, res])
.catch((err) => [err, null])
}
window.onload = async () => {
let res = await awaitWraper(getData(3))
console.log(res) // ["getdata error",null]
}

好多了,就先这样吧。

原文链接:https://zhuanlan.zhihu.com/p/114487312

收起阅读 »

先看看 VS Code Extension 知识点,再写个 VS Code 扩展玩玩

TL;DR文章篇幅有点长 ,可以先收藏再看 。要是想直接看看怎么写一个扩展,直接去第二部分 ,或者直接去github看源码 。第一部分 --- Extension 知识点一、扩展的启动如何保证性能 --- 扩展激活(Extension Activat...
继续阅读 »

TL;DR

文章篇幅有点长 ,可以先收藏再看 。要是想直接看看怎么写一个扩展,直接去第二部分 ,或者直接去github看源码 。

第一部分 --- Extension 知识点

一、扩展的启动

  1. 如何保证性能 --- 扩展激活(Extension Activation) 我们会往VS Code中安装非常多的扩展,VS Code是如何保证性能的呢? 在VS Code中有一个扩展激活(Extension Activation)的概念:VS Code会尽可能晚的加载扩展(懒加载),并且不会加载会话期间未使用的扩展,因此不会占用内存。为了完成扩展的延迟加载,VS Code定义了所谓的激活事件(activation events)。 VS Code根据特定活动触发激活事件,并且扩展可以定义需要针对哪些事件进行激活。例如,仅当用户打开Markdown文件时,才需要激活用于编辑Markdown的扩展名。
  2. 如何保证稳定性 --- 扩展隔离(Extension Isolation) 很多扩展都写得很棒 ,但是有的扩展有可能会影响启动性能或VS Code本身的整体稳定性。作为一个编辑器用户可以随时打开,键入或保存文件,确保响应性UI不受扩展程序在做什么的影响是非常重要的。 为了避免扩展可能带来的这些负面问题,VS Code在单独的Node.js进程(扩展宿主进程extension host process)中加载和运行扩展,以提供始终可用的,响应迅速的编辑器。行为不当的扩展程序不会影响VS Code,尤其不会影响其启动时间 。

四、Activation Events --- package.json

既然扩展是延迟加载(懒加载)的,我们就需要向VS Code提供有关何时应该激活什么扩展程序的上下文,其中比较重要的几个: - onLanguage:${language} - onCommand:${command} - workspaceContains:${toplevelfilename} - *

activationEvents.onLanguage

根据编程语言确定时候激活。比如我们可以这样:

"activationEvents": [
"onLanguage:javascript"
]

当检测到是js的文件时,就会激活该扩展。

activationEvents.onCommand

使用命令激活。比如我们可以这样:

"activationEvents": [
"onCommand:extension.sayHello"
]

activationEvents.workspaceContains

文件夹打开后,且文件夹中至少包含一个符合glob模式的文件时激活。比如我们可以这样:

"activationEvents": [
"workspaceContains:.editorconfig"
]

当打开的文件夹含有.editorconfig文件时,就会激活该扩展。

activationEvents.*

每当VS Code启动,就会激活。比如我们可以这样:

"activationEvents": [
"*"
]

五、Contribution Points --- package.json

其中配置的内容会暴露给用户,我们扩展大部分的配置都会写在这里: - configuration - commands - menus - keybindings - languages - debuggers - breakpoints - grammars - themes - snippets - jsonValidation - views - problemMatchers - problemPatterns - taskDefinitions - colors

contributes.configuration

在configuration中配置的内容会暴露给用户,用户可以从“用户设置”和“工作区设置”中修改你暴露的选项。 configuration是JSON格式的键值对,VS Code为用户提供了良好的设置支持。 你可以用vscode.workspace.getConfiguration('myExtension')读取配置值。

contributes.commands

设置命令标题和命令,随后这个命令会显示在命令面板中。你也可以加上category前缀,在命令面板中会以分类显示。

注意:当调用命令时(通过组合键或者在命令面板中调用),VS Code会触发激活事件onCommand:${command}。

六、package.json其他比较特殊的字段

  • engines:说明扩展程序将支持哪些版本的VS Code
  • displayName:在左侧显示的扩展名
  • icon:扩展的图标
  • categories:扩展所属的分类。可以是:Languages, Snippets, Linters, Themes, Debuggers, Formatters, Keymaps, Other

第二部分 --- 自己写个扩展玩玩

我们经常使用console.log来打印日志进行调试,我们就写一个用来美化、简化console.log的扩展玩玩。最终实现的效果:

special-console-log

实现这个扩展,需要注意以下几点: 1. console.log使用css样式 2. VS Code插入内容 3. VS Code光标和选区 4. VS Code删除内容 5. VS Code读取用户配置

下面火速实操(p≧w≦q)。

如何开始

要开始写VS Code扩展,需要两个工具:

  • yeoman:有助于启动新项目
  • vscode-generator-code:由VS Code团队使用yeoman构建的生成器 可以使用yarn或npm安装这两个工具,安装完成之后执行yo code,等一会之后它会帮我们生成起始项目,并会询问几个问题:


确认信息之后,会帮我们初始化好整个项目,此时的目录结构是这样的:


我们只需要关注src/extension.tspackage.json即可,其中package.json里面的内容之前已经介绍过。

console.log使用css样式

这里有一篇比较完整的文章:https://www.telerik.com/blogs/how-to-style-console-log-contents-in-chrome-devtools 简单的说,这句代码执行之后打印的是下面图片那样console.log("%cThis is a green text", "color:green");:



后面的样式会应用在%c后面的内容上

vscode扩展读取用户配置

上文提到过,我们可以在contributes里面定义用户配置:

"contributes": {
"configuration": {
"type": "object",
"title": "Special-console.log",
"properties": {
"special-console.log.projectName": {
"type": "string",
"default": "MyProject",
"description": "Project name"
},
"special-console.log.showLine": {
"type": "boolean",
"default": true,
"description": "Show line number"
},
"special-console.log.deleteAll": {
"type": "boolean",
"default": false,
"description": "delete all logs or delete the log containing [color] and [background]"
}
}
}
},

然后使用vscode.workspace.getConfiguration()读取用户配置

激活扩展

前面提到扩展是延迟加载(懒加载)的,我们只需要向VS Code提供有关何时应该激活什么扩展程序的上下文即可。我们在package.json中定义两个激活的事件:

"activationEvents": [
"onCommand:extension.insertLog",
"onCommand:extension.deleteLogs"
],

接着在contributes中添加快捷键:

"keybindings": [
{
"command": "extension.insertLog",
"key": "shift+ctrl+l",
"mac": "shift+cmd+l",
"when": "editorTextFocus"
},
{
"command": "extension.deleteLogs",
"key": "shift+ctrl+d",
"mac": "shift+cmd+d"
}
],

还可以将命令添加到命令面板里面,也就是按Ctrl +Shift+P弹出来的面板:

"commands": [
{
"command": "extension.insertLog",
"title": "Insert Log"
},
{
"command": "extension.deleteLogs",
"title": "Delete console.log"
}
],

insertLog表示往内容中插入console.logdeleteLogs则表示删除。具体的实现我们放到src/extension.ts的activate中:

export function activate(context: vscode.ExtensionContext) {
const insertLog = vscode.commands.registerCommand('extension.insertLog', () => {})
context.subscriptions.push(insertLog)

const deleteLogs = vscode.commands.registerCommand('extension.deleteLogs', () => {})
context.subscriptions.push(deleteLogs)
}

插入console.log

  1. 插入console.log 大概的过程是获取当前选区的内容,获取用户配置,根据用户配置和当前选区的内容填充console.log,最后插入到选区的下一行。
const insertLog = vscode.commands.registerCommand('extension.insertLog', () => {
const editor = vscode.window.activeTextEditor
if (!editor) { return }

const selection = editor.selection
const text = editor.document.getText(selection) // 当前选区内容

// 用户配置
if (userConfig) {
projectName = userConfig.projectName || projectName
showLine = userConfig.showLine || showLine
line = showLine?`%cline:${lineNumber}`:'%c'
}
// 设置console.log
...
// 在下一行插入
vscode.commands.executeCommand('editor.action.insertLineAfter')
.then(() => {
insertText(logToInsert, !text, noTextStr.length)
})
})

插入内容:

const insertText = (val: string, cursorMove: boolean, textLen: number) => {
const editor = vscode.window.activeTextEditor
if (!editor) {
vscode.window.showErrorMessage('Can\'t insert log because no document is open')
return
}
editor.edit((editBuilder) => {
editBuilder.replace(range, val) // 插入内容
}).then(() => {
// 修改选区
})
}

删除console.log

删除的时候只需要遍历找一下console.log在判断一下是不是我们加入的内容,是就删除

const deleteLogs =      vscode.commands.registerCommand('extension.deleteLogs', () => {
const editor = vscode.window.activeTextEditor
if (!editor) { return }

const document = editor.document
const documentText = editor.document.getText()

let workspaceEdit = new vscode.WorkspaceEdit()

// 获取log
const logStatements = getAllLogs(document, documentText)
// 删除
deleteFoundLogs(workspaceEdit, document.uri, logStatements)
})

删除的时候可以使用workspaceEdit.delete(docUri, log),当然,删除之后我们可以右下角搞个弹窗提示一下用户删除了几个console.log

vscode.workspace.applyEdit(workspaceEdit).then(() => {
vscode.window.showInformationMessage(`${logs.length} console.log deleted`)
})

具体的代码可以看看github

发布

这个就注册一下账号然后发布就行

原文链接:https://zhuanlan.zhihu.com/p/320220574

收起阅读 »

react中的组件设计

react的组件设计有很多模式。下面列举几种常见的:完全受控组件这种组件设计的特点是,组件的所有state和action都来自props,组件自身没有状态,只负责展示UI层。model层完全交给全局状态管理库比如redux或mobx。不推荐这种组件设计,因为后...
继续阅读 »

react的组件设计有很多模式。下面列举几种常见的:

完全受控组件

这种组件设计的特点是,组件的所有state和action都来自props,组件自身没有状态,只负责展示UI层。model层完全交给全局状态管理库比如redux或mobx。不推荐这种组件设计,因为后期不好维护。这是典型的滥用全局状态管理库的现象。

什么叫滥用全局状态管理库?

就是没有认识到状态管理库的作用,或者说我们什么时候需要状态管理库?

拿 react 来说,react 是有组内状态的,状态可以通过 props 传递。但是,但当 app 比较庞大的时候,兄弟组件,远亲组件这些的交流就变得困难起来,
它们必须依赖相同的父组件来完成信息的传递。这时,就是我们使用状态管理库的时候。

但是,很多人把所有状态都往 redux 里面丢,虽然这方便了开发,但缺点却很明显:

  1. 组件很难复用:因为状态只有一份。
  2. 耦合度高:根据高内聚低耦合的设计原则,一个模块应该有独立的功能,不依赖外部,在内部实现复杂度,只暴露接口来与外界交流。但如果把组内的一些状态放在全局 model,就提供了让其他组件修改的能力,并且代码没有内聚。

非受控组件

划分好状态的等级,尽量把状态放在组件内。当遇到共享组内状态困难的场景时,提升状态到全局状态管理库。

这种组件,有view层、model层、services层。因为它是有独立功能的,然后通过向外界暴露api来提供自己的能力,同时把复杂度隐藏在内部。

例如一个列表组件:

// 方案一
// ListDemo.jsx
import React,{useEffect} from 'react';
import {getData} from 'services/api';

export default function ListDemo({requestId}){
// model
const [data,setData] = useState([]);
const [visible,setVisible] = useState(false);

useEffect(()=>{
// services 层
getData().then(data=>{
setData(data)
});
/**
* 当requestId变化时,列表会重新请求
* 这里的requestId是组件向外界暴露的一个api
**/
},[requestId])

useEffect(()=>{
if(visible===true){
// clearState
setVisible(false);
}
},[requestId])

return (
// view
<div>
{
data.map(item=><li>{item}</li>
}
{
visible && (
<div>
this is a modal
</div>
)
}
</div>
)
)
}

// app.jsx
<ListDemo />

这种组件设计的特点是,组件可以重置自身状态的时机是由自身控制的。如果你觉得这样麻烦,你可以把重置自身状态的时机交给外部,通过key来 “销毁组件”=>“重新渲染组件”。上面的代码可以简化成:

// 方案二
// ListDemo.jsx
import React,{useEffect} from 'react';
import {getData} from 'services/api';

export default function ListDemo({requestId}){
// model
const [data,setData] = useState([]);
const [visible,setVisible] = useState(false);

useEffect(()=>{
// services 层
getData().then(data=>{
setData(data)
});
},[])

return (
// view
<div>
{
data.map(item=><li>{item}</li>
}
{
visible && (
<div>
this is a modal
</div>
)
}
</div>
)
)
}

// app.jsx
/*
*当requestId变化时,ListDemo会重新渲染
*/
<ListDemo key={requestId} />

方案二的代码比较整洁,且出错率比方案一低,但是方案二存在重新渲染组件的一个环节,性能开支会比方案一多一点点(大部分情况你都可以忽略不计)。

很多情况下,我们应该采用方案二。

原文:https://zhuanlan.zhihu.com/p/88593781

收起阅读 »

如何用webpack优化moment.js的体积

本篇为转译,原出处。当你在代码中写了var moment = require('moment') 然后再用webpack打包, 打出来的包会比你想象中的大很多,因为打包结果包含了各地的local文件.解决方案是下面的两个webpack插件,任选其一:...
继续阅读 »

本篇为转译,原出处

当你在代码中写了var moment = require('moment') 然后再用webpack打包, 打出来的包会比你想象中的大很多,因为打包结果包含了各地的local文件.


解决方案是下面的两个webpack插件,任选其一:

  1. IgnorePlugin
  2. ContextReplacementPlugin

方案一:使用 IgnorePlugin插件

IgnorePlugin的原理是会移除moment的所有本地文件,因为我们很多时候在开发中根本不会使用到。 这个插件的使用方式如下:

const webpack = require('webpack');
module.exports = {
//...
plugins: [
// 忽略 moment.js的所有本地文件
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
};

那么你可能会有疑问,所有本地文件都被移除了,但我想要用其中的一个怎么办。不用担心,你依然可以在代码中这样使用:

const moment = require('moment');
require('moment/locale/ja');

moment.locale('ja');
...

这个方案被用在 create-react-app.

方案二:使用 ContextReplacementPlugin

这个方案其实跟方案一有点像。原理是我们告诉webpack我们会使用到哪个本地文件,具体做法是在插件项中这样添加ContextReplacementPlugin

const webpack = require('webpack');
module.exports = {
//...
plugins: [
// 只加载 `moment/locale/ja.js` 和 `moment/locale/it.js`
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /ja|it/),
],
};

值得注意的是,这样你就不需要在代码中再次引入本地文件了:


const moment = require('moment');
// 不需要
moment.locale('ja');
...

体积对比

对比条件:

  • webpack: v3.10.0
  • moment.js: v2.20.1

具体表现:


可见,处理后的体积小了很多。

原文链接:https://zhuanlan.zhihu.com/p/90748774

收起阅读 »

git 撤销对文件的追踪

Git
撤销暂存区(index)区的track当我们新增加文件时,使用git status会打印出:Untracked files: (use "git add ..." to include in what will be committed) ...
继续阅读 »

撤销暂存区(index)区的track

当我们新增加文件时,使用git status会打印出:

Untracked files:
(use "git add ..." to include in what will be committed)
hello.txt

nothing added to commit but untracked files present (use "git add" to track)

可见,git add 命令可以用来追踪文件。


当我们使用 git add hello.txt后,再使用git status后,会打印出:

Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: hello.txt

可见,文件已经被追踪了,只是还没提交到本地仓库。此时可以使用git restore来撤销这个追踪。

> git restore hello.txt --staged
> git status
On branch master
Your branch is up to date with 'origin/master'.

Untracked files:
(use "git add ..." to include in what will be committed)
hello.txt


撤销“已经提交到本地仓库的文件”的追踪

当一个文件(例如hello.txt)已经提交到本地仓库时。后续你再往.gitignore添加它,也不会起作用。怎么解除这种追踪呢?最常见的做法是直接删除这个文件,流程是:本地删除,提交删除这个commit到仓库。

但这样本地的也会被删除。有时我们只是想删除仓库的副本,可以使用git rm --cachedgit rm经常被用来删除工作区和暂存区的文件。它可以携带一个cache参数,作用如下(摘自文档):

git rm --cached
Use this option to unstage and remove paths only from the index. Working tree files, whether modified or not, will be left alone.
使用这个项来解除暂存区的缓存,工作区的文件将保持不动。

意思就是不会在实际上删掉这个文件,只是解除它的追踪关系。

举例:

> git rm --cached hello.txt
// rm 'hello.txt'
> git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
(use "git restore --staged ..." to unstage)
deleted: hello.txt

工作区的hello.txt还在,但已经没有被git追踪了。之后,只要我们把hello.txt添加到.gitignore后,修改hello.txt并不会产生改动。

接下来我们提交下这个改动。

git commit -m 'delete hello.txt'
[master d7a2e3e] delete hello.txt
1 files changed, 17 deletions(-)
delete mode 100644 hello.txt

使用rm这个命令时,我们经常会用到-r这个命令。-r是递归的意思,表示删除整个文件夹,包括它的子文件夹。


原文:https://zhuanlan.zhihu.com/p/139950341

收起阅读 »

web前端常见的三种manifest文件

manifest.jsonmanifest.json经常被用在PWA,用来 告知浏览器 关于PWA应用的一些信息如应用图标、启动应用的画面。举例:{ "short_name": "React App", "name": "Creat...
继续阅读 »

manifest.json

manifest.json经常被用在PWA,用来 告知浏览器 关于PWA应用的一些信息如应用图标、启动应用的画面。举例:

{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

assets-manifest.json

assets-manifest.json经常会在create-react-app这个脚手架的打包文件上看到,由webpack-manifest-plugin这个webpack插件产生。举例:

{
"files": {
"main.css": "/static/css/main.491bee12.chunk.css",
"main.js": "/static/js/main.14bfbead.chunk.js",
"main.js.map": "/static/js/main.14bfbead.chunk.js.map",
"runtime-main.js": "/static/js/runtime-main.e89362ac.js",
"runtime-main.js.map": "/static/js/runtime-main.e89362ac.js.map",
"static/js/2.017bb613.chunk.js": "/static/js/2.017bb613.chunk.js",
"static/js/2.017bb613.chunk.js.map": "/static/js/2.017bb613.chunk.js.map",
"index.html": "/index.html",
"precache-manifest.33b41575e0c64a21bca1a6091e8a5c6d.js": "/precache-manifest.33b41575e0c64a21bca1a6091e8a5c6d.js",
"service-worker.js": "/service-worker.js",
"static/css/main.491bee12.chunk.css.map": "/static/css/main.491bee12.chunk.css.map",
"static/media/logo.svg": "/static/media/logo.25bf045c.svg"
},
"entrypoints": [
"static/js/runtime-main.e89362ac.js",
"static/js/2.017bb613.chunk.js",
"static/css/main.491bee12.chunk.css",
"static/js/main.14bfbead.chunk.js"
]
}

wepack-mainfest-plugin对它自身的介绍是:

This will generate amanifest.jsonfile in your root output directory with a mapping of all source file names to their corresponding output file。

意思就是assets-manifest.json其实只是源文件和加哈希后文件的一个对比表,仅此而已。它不会对应用的运行产生任何影响,浏览器也不会去请求它。


precache-manifest.js

这个文件由workbox-webpack-plugin插件生成, 用来告诉workbox哪些静态文件可以缓存。例如:

/**
* The workboxSW.precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
self.__precacheManifest = [].concat(self.__precacheManifest || []);
workbox.precaching.precacheAndRoute(self.__precacheManifest, {});

其中self.__precacheManifest的值就是precache-manifest.js的内容。

 原文链接:https://zhuanlan.zhihu.com/p/90829472

收起阅读 »

谈谈react hooks的优缺点

谈一下个人认为的react hooks的优缺点,通过和传统的React.Component进行对比得出。#优点一、更容易复用代码这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks...
继续阅读 »

谈一下个人认为的react hooks的优缺点,通过和传统的React.Component进行对比得出。

#优点

一、更容易复用代码

这点应该是react hooks最大的优点,它通过自定义hooks来复用状态,从而解决了类组件有些时候难以复用逻辑的问题。hooks是怎么解决这个复用的问题呢,具体如下:

  1. 每调用useHook一次都会生成一份独立的状态,这个没有什么黑魔法,函数每次调用都会开辟一份独立的内存空间。
  2. 虽然状态(from useState)和副作用(useEffect)的存在依赖于组件,但它们可以在组件外部进行定义。这点是class component做不到的,你无法在外部声明state和副作用(如componentDidMount)。

上面这两点,高阶组件和renderProps也同样能做到。但hooks实现起来的代码量更少,以及更直观(代码可读性)。

举个例子,经常使用的antd-table,用的时候经常需要维护一些状态 ,并在合适的时机去更改它们:

componentDidMount(){
this.loadData();
}

loadData = ()=>{
this.setState({
current: xxx,
total: xxx,
pageSize: xxx,
dataSource: xxx[]
})
}

onTableChange = ()=>{
this.setState({
current: xxx,
total: xxx,
pageSize: xxx,
})
}

render(){
const {total,pageSize,current,dataSource} = this.state;
return <Table
dataSource={dataSource}
pagination={{total,pageSize,current}
onChange={this.onTableChange}
/>
}

每个table都要写一些这种逻辑,那还有啥时间去摸鱼。这些高度类似的逻辑,可以通过封装一个高阶组件来抽象它们。这个高阶组件自带这些状态,并可以自动调用server去获取remote data。

用高阶组件来实现的话会是这样:

import { Table } from 'antd'
import server from './api'

function useTable(server) {
return function (WrappedComponent) {
return class HighComponent extends React.Component {
state = {
tableProps: xxx,
};
render() {
const { tableProps } = this.state;
return <WrappedComponent tableProps={tableProps} />;
}
};
};
}


@useTable(server)
class App extends Component{
render(){
/**
* 高阶组件/renderProps是通过增强组件的props(赋予一个新的属性或者方法到组件的props属性),
* 实现起来比较隐式。你难以区分这个props是来自哪个高阶组件(特别是使用了较多的高阶组件时),
* 或者还是来自业务的父组件。
*/
const { tableProps } = this.props;
return (
<Table
columns={[...]}
// tableProps包含pagination, onChange, dataSource等属性。
{...tableProps}
/>
)
}
}

如果用hooks来实现的话,会是:

import { Table } from 'antd'
import server from './api'

function useTable(server) {
const [tableProps, setTableProps] = useState(xxx);
return tableProps;
}

function App {
const { tableProps } = useTable();
return (
<Table
columns={[...]}
// tableProps包含pagination, onChange, dataSource等属性
{...tableProps}
/>
)
}
/*
相对比高阶组件“祖父=>父=>子”的层层嵌套,
hooks是这样的:
const { brother1 } = usehook1;
const { brother2} = usehook2;
*/

可以看到,hooks的逻辑更清晰,可读性更好。

二、清爽的代码风格+代码量更少

1. 函数式编程风格,函数式组件、状态保存在运行环境、每个功能都包裹在函数中,整体风格更清爽,更优雅。

2. 对IDE更友好,对比类组件,函数组件里面的unused状态和unused-method更容易被编辑器发现。

3. 使用typescript的话,类型声明也变得更容易。

class Example{
hello: string;
constructor(){
this.hello = 'hello world'
}
}

// 代码量更少
function Example(){
const hello:string = 'hello world'
}


4. 向props或状态取值更加方便,函数组件的取值都从当前作用域直接获取变量,而类组件需要先访问实例this,再访问其属性或者方法,多了一步。

5. 更改状态也变得更加简单, `this.setState({ count:xxx })`变成 `setCount(xxx)`。


因为减少了很多模板代码,特别是小组件写起来更加省事,人们更愿意去拆分组件。而组件粒度越细,被复用的可能性越大。所以,hooks也在不知不觉中改变人们的开发习惯,提高项目的组件复用率。


#缺点

一、响应式的useEffect

写函数组件时,你不得不改变一些写法习惯。你必须清楚代码中useEffectuseCallback的“依赖项数组”的改变时机。有时候,你的useEffect依赖某个函数的不可变性,这个函数的不可变性又依赖于另一个函数的不可变性,这样便形成了一条依赖链。一旦这条依赖链的某个节点意外地被改变了,你的useEffect就被意外地触发了,如果你的useEffect是幂等的操作,可能带来的是性能层次的问题,如果是非幂等,那就糟糕了。

所以,对比componentDidmountcomponentDidUpdate,useEffect带来的心智负担更大。

二、hooks不擅长异步的代码(旧引用问题)

函数的运行是独立的,每个函数都有一份独立的作用域。函数的变量是保存在运行时的作用域里面。当我们有异步操作的时候,经常会碰到异步回调的变量引用是之前的,也就是旧的(这里也可以理解成闭包)。比如下面的一个例子(codesandbox):

import React, { useState } from "react";

const Counter = () => {
const [counter, setCounter] = useState(0);

const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counter);
}, 3000);
};

return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};

export default Counter;

当你点击Show me the value in 3 seconds的后,紧接着点击Click me使得counter的值从0变成1。三秒后,定时器触发,但alert出来的是0(旧值),但我们希望的结果是当前的状态1。

这个问题在class component不会出现,因为class component的属性和方法都存放在一个instance上,调用方式是:this.state.xxxthis.method()。因为每次都是从一个不变的instance上进行取值,所以不存在引用是旧的问题。

其实解决这个hooks的问题也可以参照类的instance。用useRef返回的immutable RefObject(把值保存在current属性上)来保存state,然后取值方式从counter变成了: counterRef.current。如下:

import React, { useState, useRef, useEffect } from "react";

const Counter = () => {
const [counter, setCounter] = useState(0);
const counterRef = useRef(counter);

const onAlertButtonClick = () => {
setTimeout(() => {
alert("Value: " + counterRef.current);
}, 3000);
};

useEffect(() => {
counterRef.current = counter;
});

return (
<div>
<p>You clicked {counter} times.</p>
<button onClick={() => setCounter(counter + 1)}>Click me</button>
<button onClick={onAlertButtonClick}>
Show me the value in 3 seconds
</button>
</div>
);
};

export default Counter;

结果如我们所期待,alert的是当前的值1。

我们可以把这个过程封装成一个custom hook,如下:

import { useEffect, useRef, useState } from "react";

const useRefState = <T>(
initialValue: T
): [T, React.MutableRefObject<T>, React.Dispatch<React.SetStateAction<T>>] => {
const [state, setState] = useState<T>(initialValue);
const stateRef = useRef(state);
useEffect(() => {
stateRef.current = state;
}, [state]);
return [state, stateRef, setState];
};

export default useRefState;

尽管这个问题被巧妙地解决了,但它不优雅、hack味道浓,且丢失了函数编程风格。


三、custom hooks有时严重依赖参数的不可变性

import {useState, useEffect} from 'react'

export function() useData(api){
const [data, setDate] = useState([]);
useEffect(()=>{
api().then(res=>setData(res.data)) ;
// 这里要求传入的api是immutable的,被useCallback/useMemo所包裹。不然每次api一变,
// 都会非预期地多调用一次useEffect。
},[api])
}

对于这点,除了在团队约定参数的不可变性,还可以对useCallback/useMemo进行魔改:

import React from "react";

let useCallback = React.useCallback;

if (__DEV__) {
useCallback = (fn, arr) => {
fn.__useCallback__ = true;
return useCallback(fn, arr);
};
}

export default useCallback;

然后在run-time中去检查是否存在__useCallback__这个属性:

import {useState, useEffect} from 'react'

function checkFn(fn){
if(__DEV__){
if(!fn.__useCallback__){
throw Error('请用团队封装的useCallback来包裹fn')
}
}
}

export function() useData(api){
const [data, setDate] = useState([]);

checkFn(api);

useEffect(()=>{
api().then(res=>setData(res.data)) ;
// 这里要求传入的api是immutable的,被useCallback/useMemo所包裹。不然每次api一变,
// 都会非预期地多调用一次useEffect。
},[api])
}

也有其他的方案:比如用eslint插件去检查。


#怎么避免react hooks的常见问题

  1. 不要在useEffect里面写太多的依赖项,划分这些依赖项成多个单一功能的useEffect。其实这点是遵循了软件设计的“单一职责模式”。
  2. 如果你碰到状态不同步的问题,可以考虑下手动传递参数到函数。如:
   // showCount的count来自父级作用域 
const [count,setCount] = useState(xxx);
function showCount(){ console.log(count) }

// showCount的count来自参数
const [count,setCount] = useState(xxx);
function showCount(c){ console.log(c) }

但这个也只能解决一部分问题,很多时候你不得不使用上述的useRef方案。

3. 拆分组件,细化组件的粒度。复杂业务场景中使用hooks,应尽可能地细分组件,使得组件的功能尽可能单一,这样的hooks组件更好维护。


#感想

hooks很好用很强大,但它不擅长异步。但在有太多异步逻辑的代码时,class比它更适合、更稳、更好维护。


原文链接:https://zhuanlan.zhihu.com/p/88593858

收起阅读 »

React Hooks究竟是什么呢?

我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件却不能?React 早期版本,类组件可以通过继承PureComponent来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以...
继续阅读 »

我们大部分 React 类组件可以保存状态,而函数组件不能? 并且类组件具有生命周期,而函数组件却不能?

React 早期版本,类组件可以通过继承PureComponent来优化一些不必要的渲染,相对于函数组件,React 官网没有提供对应的方法来缓存函数组件以减少一些不必要的渲染,直接 16.6 出来的 React.memo函数。

React 16.8 新出来的Hook可以让React 函数组件具有状态,并提供类似 componentDidMountcomponentDidUpdate等生命周期方法。

类被会替代吗?

Hooks不会替换类,它们只是一个你可以使用的新工具。React 团队表示他们没有计划在React中弃用类,所以如果你想继续使用它们,可以继续用。

我能体会那种总有新东西要学的感觉有多痛苦,不会就感觉咱们总是落后一样。Hooks 可以当作一个很好的新特性来使用。当然没有必要用 Hook 来重构原来的代码, React团队也建议不要这样做。

Go Go

来看看Hooks的例子,咱们先从最熟悉的开始:函数组件。

以下 OneTimeButton 是函数组件,所做的事情就是当我们点击的时候调用 sayHi 方法。

import React from 'react';
import { render } from 'react-dom';

function OneTimeButton(props) {
return (
<button onClick={props.onClick}>
点我点我
</button>
)
}

function sayHi() {
console.log('yo')
}

render(
<OneTimeButton onClick={sayHi}/>,
document.querySelector('#root')
)

我们想让这个组件做的是,跟踪它是否被点击,如果被点击了,禁用按钮,就像一次性开关一样。

但它需要一个state,因为是一个函数,它不可能有状态(React 16.8之前),所以需要重构成类。

函数组件转换为类组件的过程中大概有5个阶段:

  • 否认:也许它不需要是一个类,我们可以把 state 放到其它地方。

  • 实现: 废话,必须把它变成一个class,不是吗?

  • 接受:好吧,我会改的。

  • 努力加班重写:首先 写 class Thing extends React.Component,然后 实现 render等等 。

  • 最后:添加state。


class OneTimeButton extends React.Component {
state = {
clicked: false
}

handleClick = () => {
this.props.onClick();

// Ok, no more clicking.
this.setState({ clicked: true });
}

render() {
return (
<button
onClick={this.handleClick}
disabled={this.state.clicked}
>
You Can Only Click Me Once
</button>
);
}
}

这是相当多的代码,组件的结构也发生了很大的变化, 我们需要多个小的功能,就需要改写很多。

使用 Hook 轻松添加 State

接下来,使用新的 useState hook向普通函数组件添加状态:

import React, { useState } from 'react'

function OneTimeButton(props) {
const [clicked, setClicked] = useState(false)

function doClick() {
props.onClick();
setClicked(true)
}

return (
<button
onClick={clicked ? undefined : doClick}
disabled={clicked}
>
点我点我
</button>
)
}

这段代码是如何工作的

这段代码的大部分看起来像我们一分钟前写的普通函数组件,除了useState

useState是一个hook。 它的名字以“use”开头(这是Hooks的规则之一 - 它们的名字必须以“use”开头)。

useState hook 的参数是 state 的初始值,返回一个包含两个元素的数组:当前state和一个用于更改state 的函数。

类组件有一个大的state对象,一个函数this.setState一次改变整个state对象。

函数组件根本没有状态,但useState hook允许我们在需要时添加很小的状态块。 因此,如果只需要一个布尔值,我们就可以创建一些状态来保存它。

由于Hook以某种特殊方式创建这些状态,并且在函数组件内也没有像setState函数来更改状态,因此 Hook 需要一个函数来更新每个状态。 所以 useState 返回是一对对应关系:一个值,一个更新该值函数。 当然,值可以是任何东西 - 任何JS类型 - 数字,布尔值,对象,数组等。

现在,你应该有很多疑问,如:

  • 当组件重新渲染时,每次都不会重新创建新的状态吗? React如何知道旧状态是什么?

  • 为什么hook 名称必须以“use”开头? 这看起来很可疑。

  • 如果这是一个命名规则,那是否意味着我可以自定义 Hook。

  • 如何存储更复杂的状态,很多场景不单单只有一个状态值这么简单。

Hooks 的魔力

将有状态信息存储在看似无状态的函数组件中,这是一个奇怪的悖论。这是第一个关于钩子的问题,咱们必须弄清楚它们是如何工作的。

原作者得的第一个猜测是某种编译器的在背后操众。搜索代码useWhatever并以某种方式用有状态逻辑替换它。

然后再听说了调用顺序规则(它们每次必须以相同的顺序调用),这让我更加困惑。这就是它的工作原理。

React第一次渲染函数组件时,它同时会创建一个对象与之共存,该对象是该组件实例的定制对象,而不是全局对象。只要组件存在于DOM中,这个组件的对象就会一直存在。

使用该对象,React可以跟踪属于组件的各种元数据位。

请记住,React组件甚至函数组件都从未进行过自渲染。它们不直接返回HTML。组件依赖于React在适当的时候调用它们,它们返回的对象结构React可以转换为DOM节点。

React有能力在调用每个组件之前做一些设置,这就是它设置这个状态的时候。

其中做的一件事设置 Hooks 数组。 它开始是空的, 每次调用一个hook时,React 都会向该数组添加该 hook

为什么顺序很重要

假设咱们有以下这个组件:

function AudioPlayer() {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);

.....
}

因为它调用useState 3次,React 会在第一次渲染时将这三个 hook 放入 Hooks 数组中。

下次渲染时,同样的3hooks以相同的顺序被调用,所以React可以查看它的数组,并发现已经在位置0有一个useState hook ,所以React不会创建一个新状态,而是返回现有状态。

这就是React能够在多个函数调用中创建和维护状态的方式,即使变量本身每次都超出作用域。

多个useState 调用示例

让咱们更详细地看看这是如何实现的,第一次渲染:

  1. React 创建组件时,它还没有调用函数。React 创建元数据对象和Hooks的空数组。假设这个对象有一个名为nextHook的属性,它被放到索引为0的位置上,运行的第一个hook将占用位置0
  1. React 调用你的组件(这意味着它知道存储hooks的元数据对象)。
  1. 调用useState,React创建一个新的状态,将它放在hooks数组的第0位,并返回[volume,setVolume]对,并将volume 设置为其初始值80,它还将nextHook索引递增1。

  2. 再次调用useState,React查看数组的第1位,看到它是空的,并创建一个新的状态。 然后它将nextHook索引递增为2,并返回[position,setPosition]

  3. 第三次调用useState。 React看到位置2为空,同样创建新状态,将nextHook递增到3,并返回[isPlaying,setPlaying]

现在,hooks 数组中有3个hook,渲染完成。 下一次渲染会发生什么?

  1. React需要重新渲染组件, 由于 React 之前已经看过这个组件,它已经有了元数据关联。

  2. ReactnextHook索引重置为0,并调用组件。

  3. 调用useState,React查看索引0处的hooks数组,并发现它已经在该槽中有一个hook。,所以无需重新创建一个,它将nextHook推进到索引1并返回[volume,setVolume],其中volume仍设置为80

  4. 再次调用useState。 这次,nextHook1,所以React检查数组的索引1。同样,hook 已经存在,所以它递增nextHook并返回[position,setPosition]

  5. 第三次调用useState,我想你知道现在发生了什么。

就是这样了,知道了原理,看起来也就不那么神奇了, 但它确实依赖于一些规则,所以才有使用 Hooks 规则。

Hooks 的规则

自定义 hooks 函数只需要遵守规则 3 :它们的名称必须以“use”为前缀。

例如,我们可以从AudioPlayer组件中将3个状态提取到自己的自定义钩子中:

function AudioPlayer() {
// Extract these 3 pieces of state:
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);

// < beautiful audio player goes here >
}

因此,咱们可以创建一个专门处理这些状态的新函数,并使用一些额外的方法返回一个对象,以便更容易启动和停止播放,例如:

function usePlayerState(lengthOfClip) {
const [volume, setVolume] = useState(80);
const [position, setPosition] = useState(0);
const [isPlaying, setPlaying] = useState(false);

const stop = () => {
setPlaying(false);
setPosition(0);
}

const start = () => {
setPlaying(true);
}

return {
volume,
position,
isPlaying,
setVolume,
setPosition,
start,
stop
};
}

像这样提取状态的一个好处是可以将相关的逻辑和行为组合在一起。可以提取一组状态和相关事件处理程序以及其他更新逻辑,这不仅可以清理组件代码,还可以使这些逻辑和行为可重用。

另外,通过在自定义hooks中调用自定义hooks,可以将hooks组合在一起。hooks只是函数,当然,函数可以调用其他函数。

总结

Hooks 提供了一种新的方式来处理React中的问题,其中的思想是很有意思且新奇的。

React团队整合了一组很棒的文档和一个常见问题解答,从是否需要重写所有的类组件到钩Hooks是否因为在渲染中创建函数而变慢? 以及两者之间的所有东西,所以一定要看看。

原文:https://daveceddia.com/intro-to-hooks/

收起阅读 »

当面试官问Webpack的时候他想知道什么

前言在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花...
继续阅读 »

前言

在前端工程化日趋复杂的今天,模块打包工具在我们的开发中起到了越来越重要的作用,其中webpack就是最热门的打包工具之一。

说到webpack,可能很多小伙伴会觉得既熟悉又陌生,熟悉是因为几乎在每一个项目中我们都会用上它,又因为webpack复杂的配置和五花八门的功能感到陌生。尤其当我们使用诸如umi.js之类的应用框架还帮我们把webpack配置再封装一层的时候,webpack的本质似乎离我们更加遥远和深不可测了。

当面试官问你是否了解webpack的时候,或许你可以说出一串耳熟能详的webpack loaderplugin的名字,甚至还能说出插件和一系列配置做按需加载和打包优化,那你是否了解他的运行机制以及实现原理呢,那我们今天就一起探索webpack的能力边界,尝试了解webpack的一些实现流程和原理,拒做API工程师。


你知道webpack的作用是什么吗?

从官网上的描述我们其实不难理解,webpack的作用其实有以下几点:

  • 模块打包。可以将不同模块的文件打包整合在一起,并且保证它们之间的引用正确,执行有序。利用打包我们就可以在开发的时候根据我们自己的业务自由划分文件模块,保证项目结构的清晰和可读性。
  • 编译兼容。在前端的“上古时期”,手写一堆浏览器兼容代码一直是令前端工程师头皮发麻的事情,而在今天这个问题被大大的弱化了,通过webpackLoader机制,不仅仅可以帮助我们对代码做polyfill,还可以编译转换诸如.less, .vue, .jsx这类在浏览器无法识别的格式文件,让我们在开发的时候可以使用新特性和新语法做开发,提高开发效率。
  • 能力扩展。通过webpackPlugin机制,我们在实现模块化打包和编译兼容的基础上,可以进一步实现诸如按需加载,代码压缩等一系列功能,帮助我们进一步提高自动化程度,工程效率以及打包输出的质量。

说一下模块打包运行原理?

如果面试官问你Webpack是如何把这些模块合并到一起,并且保证其正常工作的,你是否了解呢?

首先我们应该简单了解一下webpack的整个打包流程:

  • 1、读取webpack的配置参数;
  • 2、启动webpack,创建Compiler对象并开始解析项目;
  • 3、从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
  • 4、对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件;
  • 5、整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的。

其中文件的解析与构建是一个比较复杂的过程,在webpack源码中主要依赖于compilercompilation两个核心对象实现。

compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。compilation对象是每一次构建的上下文对象,它包含了当次构建所需要的所有信息,每次热更新和重新构建,compiler都会重新生成一个新的compilation对象,负责此次更新的构建过程。

而每个模块间的依赖关系,则依赖于AST语法树。每个模块文件在通过Loader解析完成之后,会通过acorn库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。

最终Webpack打包出来的bundle文件是一个IIFE的执行函数。

// webpack 5 打包的bundle文件内容

(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})

// The module cache
var __webpack_module_cache__ = {};

// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};

// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");

// Return the exports of the module
return module.exports;
}

// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})

webpack4相比,webpack5打包出来的bundle做了相当的精简。在上面的打包demo中,整个立即执行函数里边只有三个变量和一个函数方法,__webpack_modules__存放了编译后的各个文件模块的JS内容,__webpack_module_cache__ 用来做模块缓存,__webpack_require__Webpack内部实现的一套依赖引入函数。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。

其中值得一提的是__webpack_require__模块引入函数,我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。

你知道sourceMap是什么吗?

提到sourceMap,很多小伙伴可能会立刻想到Webpack配置里边的devtool参数,以及对应的evaleval-cheap-source-map等等可选值以及它们的含义。除了知道不同参数之间的区别以及性能上的差异外,我们也可以一起了解一下sourceMap的实现方式。

sourceMap是一项将编译、打包、压缩后的代码映射回源代码的技术,由于打包压缩后的代码并没有阅读性可言,一旦在开发中报错或者遇到问题,直接在混淆代码中debug问题会带来非常糟糕的体验,sourceMap可以帮助我们快速定位到源代码的位置,提高我们的开发效率。sourceMap其实并不是Webpack特有的功能,而是Webpack支持sourceMap,像JQuery也支持souceMap

既然是一种源码的映射,那必然就需要有一份映射的文件,来标记混淆代码里对应的源码的位置,通常这份映射文件以.map结尾,里边的数据结构大概长这样:

{
"version" : 3, // Source Map版本
"file": "out.js", // 输出文件(可选)
"sourceRoot": "", // 源文件根目录(可选)
"sources": ["foo.js", "bar.js"], // 源文件列表
"sourcesContent": [null, null], // 源内容列表(可选,和源文件列表顺序一致)
"names": ["src", "maps", "are", "fun"], // mappings使用的符号名称列表
"mappings": "A,AAAB;;ABCDE;" // 带有编码映射数据的字符串
}

其中mappings数据有如下规则:

  • 生成文件中的一行的每个组用“;”分隔;
  • 每一段用“,”分隔;
  • 每个段由1、4或5个可变长度字段组成;

有了这份映射文件,我们只需要在我们的压缩代码的最末端加上这句注释,即可让sourceMap生效:

//# sourceURL=/path/to/file.js.map

有了这段注释后,浏览器就会通过sourceURL去获取这份映射文件,通过解释器解析后,实现源码和混淆代码之间的映射。因此sourceMap其实也是一项需要浏览器支持的技术。

如果我们仔细查看webpack打包出来的bundle文件,就可以发现在默认的development开发模式下,每个_webpack_modules__文件模块的代码最末端,都会加上//# sourceURL=webpack://file-path?,从而实现对sourceMap的支持。

sourceMap映射表的生成有一套较为复杂的规则,有兴趣的小伙伴可以看看以下文章,帮助理解soucrMap的原理实现:

Source Map的原理探究[1]

Source Maps under the hood – VLQ, Base64 and Yoda[2]

是否写过Loader?简单描述一下编写loader的思路?

从上面的打包代码我们其实可以知道,Webpack最后打包出来的成果是一份Javascript代码,实际上在Webpack内部默认也只能够处理JS模块代码,在打包过程中,会默认把所有遇到的文件都当作 JavaScript代码进行解析,因此当项目存在非JS类型文件时,我们需要先对其进行必要的转换,才能继续执行打包任务,这也是Loader机制存在的意义。

Loader的配置使用我们应该已经非常的熟悉:

// webpack.config.js
module.exports = {
// ...other config
module: {
rules: [
{
test: /^your-regExp$/,
use: [
{
loader: 'loader-name-A',
},
{
loader: 'loader-name-B',
}
]
},
]
}
}

通过配置可以看出,针对每个文件类型,loader是支持以数组的形式配置多个的,因此当Webpack在转换该文件类型的时候,会按顺序链式调用每一个loader,前一个loader返回的内容会作为下一个loader的入参。因此loader的开发需要遵循一些规范,比如返回值必须是标准的JS代码字符串,以保证下一个loader能够正常工作,同时在开发上需要严格遵循“单一职责”,只关心loader的输出以及对应的输出。

loader函数中的this上下文由webpack提供,可以通过this对象提供的相关属性,获取当前loader需要的各种信息数据,事实上,这个this指向了一个叫loaderContextloader-runner特有对象。有兴趣的小伙伴可以自行阅读源码。

module.exports = function(source) {
const content = doSomeThing2JsString(source);

// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;

// 可以用作解析其他模块路径的上下文
console.log('this.context');

/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);
// or return content;
}

更详细的开发文档可以直接查看官网的 Loader API[3]。

是否写过Plugin?简单描述一下编写plugin的思路?

如果说Loader负责文件转换,那么Plugin便是负责功能扩展。LoaderPlugin作为Webpack的两个重要组成部分,承担着两部分不同的职责。

上文已经说过,webpack基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务,从而实现自己想要的功能。

既然基于发布订阅模式,那么知道Webpack到底提供了哪些事件钩子供插件开发者使用是非常重要的,上文提到过compilercompilationWebpack两个非常核心的对象,其中compiler暴露了和 Webpack整个生命周期相关的钩子(compiler-hooks[4]),而compilation则暴露了与模块和依赖有关的粒度更小的事件钩子(Compilation Hooks[5])。

Webpack的事件机制基于webpack自己实现的一套Tapable事件流方案(github[6])

// Tapable的简单使用
const { SyncHook } = require("tapable");

class Car {
constructor() {
// 在this.hooks中定义所有的钩子事件
this.hooks = {
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}

/* ... */
}


const myCar = new Car();
// 通过调用tap方法即可增加一个消费者,订阅对应的钩子事件了
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());

Plugin的开发和开发Loader一样,需要遵循一些开发上的规范和原则:

  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例;
  • 传给每个插件的 compiler 和 compilation 对象都是同一个引用,若在一个插件中修改了它们身上的属性,会影响后面的插件;
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住;

了解了以上这些内容,想要开发一个 Webpack Plugin,其实也并不困难。

class MyPlugin {
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);

// do something...
})
}
}

最后

本文也是结合一些优秀的文章和webpack本身的源码,大概地说了几个相对重要的概念和流程,其中的实现细节和设计思路还需要结合源码去阅读和慢慢理解。

Webpack作为一款优秀的打包工具,它改变了传统前端的开发模式,是现代化前端开发的基石。这样一个优秀的开源项目有许多优秀的设计思想和理念可以借鉴,我们自然也不应该仅仅停留在API的使用层面,尝试带着问题阅读源码,理解实现的流程和原理,也能让我们学到更多知识,理解得更加深刻,在项目中才能游刃有余的应用。

参考资料

[1]Source Map的原理探究: https://blog.fundebug.com/201...

[2]Source Maps under the hood – VLQ, Base64 and Yoda: *https://docs.microsoft.com/zh...

[3]Loader API: *https://www.webpackjs.com/api...

[4]compiler-hooks: https://webpack.js.org/api/co...

[5]Compilation Hooks: https://webpack.js.org/api/co...

[6]github: https://github.com/webpack/ta...

[7]Plugin API: https://www.webpackjs.com/api...

原文地址(前端大全)

收起阅读 »

几个优雅的JavaScript运算符使用技巧

ECMAScript发展进程中,会有很多功能的更新,比如销毁,箭头功能,模块,它们极大的改变JavaScript编写方式,可能有些人喜欢,有些人不喜欢,但像每个新功能一样,我们最终会习惯它们。新版本的ECMAScript引入了三个新的逻辑赋值运算符:空运算符,...
继续阅读 »

ECMAScript发展进程中,会有很多功能的更新,比如销毁,箭头功能,模块,它们极大的改变JavaScript编写方式,可能有些人喜欢,有些人不喜欢,但像每个新功能一样,我们最终会习惯它们。新版本的ECMAScript引入了三个新的逻辑赋值运算符:空运算符,AND和OR运算符,这些运算符的出现,也是希望让我们的代码更干净简洁,下面分享几个优雅的JavaScript运算符使用技巧

一、可选链接运算符【?.】

可选链接运算符(Optional Chaining Operator) 处于ES2020提案的第4阶段,因此应将其添加到规范中。它改变了访问对象内部属性的方式,尤其是深层嵌套的属性。它也可以作为TypeScript 3.7+中的功能使用。

相信大部分开发前端的的小伙伴们都会遇到null和未定义的属性。JS语言的动态特性使其无法不碰到它们。特别是在处理嵌套对象时,以下代码很常见:

if (data && data.children && data.children[0] && data.children[0].title) {
// I have a title!
}

上面的代码用于API响应,我必须解析JSON以确保名称存在。但是,当对象具有可选属性或某些配置对象具有某些值的动态映射时,可能会遇到类似情况,需要检查很多边界条件。

这时候,如果我们使用可选链接运算符,一切就变得更加轻松了。它为我们检查嵌套属性,而不必显式搜索梯形图。我们所要做的就是使用“?” 要检查空值的属性之后的运算符。我们可以随意在表达式中多次使用该运算符,并且如果未定义任何项,它将尽早返回。

对于静态属性用法是:

object?.property

对于动态属性将其更改为:

object?.[expression]

上面的代码可以简化为:

let title = data?.children?.[0]?.title;

然后,如果我们有:


let data;
console.log(data?.children?.[0]?.title) // undefined

data = {children: [{title:'codercao'}]}
console.log(data?.children?.[0]?.title) // codercao

这样写是不是更加简单了呢? 由于操作符一旦为空值就会终止,因此也可以使用它来有条件地调用方法或应用条件逻辑


const conditionalProperty = null;
let index = 0;

console.log(conditionalProperty?.[index++]); // undefined
console.log(index); // 0

对于方法的调用你可以这样写

object.runsOnlyIfMethodExists?.()

例如下面的parent对象,如果我们直接调用parent.getTitle(),则会报Uncaught TypeError: parent.getTitle is not a function错误,parent.getTitle?.()则会终止不会执行

let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
console.log(this.name)
}
};

parent.getName?.() // parent
parent.getTitle?.() //不会执行

与无效合并一起使用

提供了一种方法来处理未定义或为空值和表达提供默认值。我们可以使用??运算符,为表达式提供默认值

console.log(undefined ?? 'codercao'); // codercao

因此,如果属性不存在,则可以将无效的合并运算符与可选链接运算符结合使用以提供默认值。

let title = data?.children?.[0]?.title ?? 'codercao';
console.log(title); // codercao

二、逻辑空分配(?? =)

expr1 ??= expr2

逻辑空值运算符仅在空值(空值或未定义undefined)时才将值分配给expr1,表达方式:

x ??= y

可能看起来等效于:

x = x ?? y;

但事实并非如此!有细微的差别。

空的合并运算符(??)从左到右操作,如果x不为空,则短路。因此,如果x不为null或未定义,则永远不会对表达式y进行求值。因此,如果y是一个函数,它将根本不会被调用。因此,此逻辑赋值运算符等效于

x ?? (x = y);

三、逻辑或分配(|| =)

此逻辑赋值运算符仅在左侧表达式为 falsy值时才赋值。Falsy值与null有所不同,因为falsy值可以是任何一种值:undefined,null,空字符串(双引号""、单引号’’、反引号``),NaN,0。IE浏览器中的 document.all,也算是一个。

语法

x ||= y

等同于

x || (x = y)

在我们想要保留现有值(如果不存在)的情况下,这很有用,否则我们想为其分配默认值。例如,如果搜索请求中没有数据,我们希望将元素的内部HTML设置为默认值。否则,我们要显示现有列表。这样,我们避免了不必要的更新和任何副作用,例如解析,重新渲染,失去焦点等。我们可以简单地使用此运算符来使用JavaScript更新HTML:

document.getElementById('search').innerHTML ||= '<i>No posts found matching this search.</i>'

四、逻辑与分配(&& =)

可能你已经猜到了,此逻辑赋值运算符仅在左侧为真时才赋值。因此:

x &&= y

等同于

x && (x = y)
最后

本次分享几个优雅的JavaScript运算符使用技巧,重点分享了可选链接运算符的使用,这样可以让我们不需要再编写大量我们例子中代码即可轻松访问嵌套属性。但是IE不支持它,因此,如果需要支持该版本或更旧版本的浏览器,则可能需要添加Babel插件。对于Node.js,需要为此升级到Node 14 LTS版本,因为12.x不支持该版本。

如果你也有优雅的优雅的JavaScript运算符使用技巧,请不要吝惜,在评论区一起交流~

原文链接:https://segmentfault.com/a/1190000039885243


收起阅读 »

uniapp你是真的坑!!

最近要做一个锚点的效果,于是用到了这个方法,先获取节点信息,然后根据节点高度与页面滚动距离得到所需滑动的距离,但是这里有一个大坑,搞了两天,百思不得其解!就是h5端是正常的,app上不行,看了文档后也不存在兼容问题,于是,这里省下一百字骂人的话。。。uni.c...
继续阅读 »

最近要做一个锚点的效果,于是用到了这个方法,先获取节点信息,然后根据节点高度与页面滚动距离得到所需滑动的距离,但是这里有一个大坑,搞了两天,百思不得其解!就是h5端是正常的,app上不行,看了文档后也不存在兼容问题,于是,这里省下一百字骂人的话。。。

uni.createSelectorQuery().in(this).select("#one").boundingClientRect(data => {
uni.pageScrollTo({
duration:200,
scrollTop: that.scrollTop + data.top-44
});
}).exec();

问题:h5上一切正常,app上只有初次事件触发—页面滚动是正常的,再次触发时,就报错,是这样的报错:

//uniappnmsl
h.push is not a function

问题解决:

//设置duration  这里是页面滚动时的滚动效果
duration:200 => duration:0,

然后
就解决了,就解决了!

uniapp 你该长大了,要学会自己更新bug了

最后,祝uniapp长命百岁,新年快乐

原文链接:https://segmentfault.com/a/1190000021222154
收起阅读 »

h5转uniapp项目技术总结

h5项目转uniapp项目总结why先说一下为什么要用uniapp,主要是因为之前我们刚做完云闪付小程序(webview页面),老板又让我们做抖音小程序(后面还会做各种小程序),于是我们就想到了uniapp,之前也没做过想想也挺激动的项目目录├── READM...
继续阅读 »

h5项目转uniapp项目总结

why

先说一下为什么要用uniapp,主要是因为之前我们刚做完云闪付小程序(webview页面),老板又让我们做抖音小程序(后面还会做各种小程序),于是我们就想到了uniapp,之前也没做过想想也挺激动的

项目目录

├── README.md

├── babel.config.js

├── dist

├── node_modules

├── package-lock.json

├── package.json

├── postcss.config.js
├── public

├── src


├── App.vue
├── api
├── assets
├── components
├── config
├── main.js
├── manifest.json
├── mixins
├── pages
├── pages.json
├── pagesub
├── services
├── static
├── store
├── uni.scss
└── utils

├── tsconfig.json

├── vue.config.js

├── yarn-error.log

└── yarn.lock

条件编译

/** #ifdef 平台名称 **/ 

你的css或者js代码

/** #endif **/

样式

scoped 样式穿透
/deep/ 选择器 {}

// vue.config 配置less全局变量引入
let path = require('path');
module.exports = {
// 全局使用less变量
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [
path.resolve(__dirname, 'src/assets/theme.less') // 我的主题变量文件地址
]
}
}
}


插件

符合easycom命名规范可以省略引入组件的步骤

vuex

store数据改变页面未更新,我使用了一个mixin来解决,大概思路是混入需要更新的属性,在onShow钩子函数中执行
// mixin
export default {
data() {
return {
userInfo: {}
}
},
methods: {
getUserInfo() {
this.userInfo = this.$store.getters.userInfo
}
}
}

// 页面 重新赋值
onShow() {
this.getUserInfo()
}


路由

  • Vue的路由全写在pages里面
  • 路由跳转使用uniapp api
  • 页面title设置参考uniapp API

原生组件层级过高

页面的textarea层级过高盖住了popup弹窗

  • 使用cover-view提高popup组件层级(头条小程序不支持cover-view)
  • 使用hidden属性动态显示隐藏原生组件,popup组件弹出隐藏原生组件,反之亦然

最后

一开始写是愉悦的,改样式bug是痛苦的,结局是还算是好的。

收起阅读 »

mpvue不维护了,已经成型的mpvue项目怎么办

mpvue作为美团技术团队的一个KPI产品,莫名其妙突然就不维护了,随着node版本和项目依赖不断更新,你会发现你原先的项目可能突然跑不起来了,此时你需要mpvue的替代品,没错,我要说的就是uni-app。只需几步,你可以很轻松的把mpvue项目迁移到uni...
继续阅读 »

mpvue作为美团技术团队的一个KPI产品,莫名其妙突然就不维护了,随着node版本和项目依赖不断更新,你会发现你原先的项目可能突然跑不起来了,此时你需要mpvue的替代品,没错,我要说的就是uni-app。

只需几步,你可以很轻松的把mpvue项目迁移到uni-app。

先去官网按步骤建好项目

https://uniapp.dcloud.io/quickstart

1、 把mpvue项目里src目录的文件复制到uni-app项目里


2、把main.js搬到uniapp的page.json里

mpvue的main.js


搬过来之后是这样的


3、运行,看看css是否跟原版有偏差,重新调整。此外要把api改成uni-app的,例如发请求api的要换成这个

https://uniapp.dcloud.io/api/request/request



收起阅读 »

JS实现精确倒计时

实现倒计时对前端工程师来说,是很常见的需求。那么,要怎么实现精确的倒计时呢?首先,考虑到客户端时间和服务端时间有误差,所以计算倒计时的时候,应该读取服务端的时间。但是,只考虑到这一点还远远不够的。页面运行时间长了,新打开页面的倒计时和原打开页面的倒计时还是存在...
继续阅读 »

实现倒计时对前端工程师来说,是很常见的需求。那么,要怎么实现精确的倒计时呢?

首先,考虑到客户端时间和服务端时间有误差,所以计算倒计时的时候,应该读取服务端的时间。但是,只考虑到这一点还远远不够的。页面运行时间长了,新打开页面的倒计时和原打开页面的倒计时还是存在误差。要减少这里的误差,就要说到Javascript解释器的工作原理。

Javascript解释器工作原理
Javascript解释器是单线程工作的,它执行任务按照任务进入队列的先后顺序执行。这会造成什么影响呢?
打个比方,设置定时器的时候,按照理想状况,下面的程序应当稳定的输出0。

let start = new Date().getTime()
let count = 0
setInterval(function(){
count++
console.log(new Date().getTime() - (start + count * 1000))
},1000)


由于代码执行占用时间,以及其他事件的阻塞,导致有些事件的执行延迟了几ms,阻塞事件不多时,影响微乎其微。但当我们添加更多的阻塞事件时,这个影响就会被放大,如下面的代码

let start = new Date().getTime()
let count = 0
setInterval(function(){
let j = 0
while(j++ < 100000000){}
},1)
setInterval(function(){
count++
console.log(new Date().getTime() - (start + count * 1000))
},1000)


线程阻塞解决方案

那么我们要怎么解决线程阻塞的问题呢?
按照正常的思路,如果没有被阻塞,下面设置好的定时器应该每隔1s执行一次。

setInterval(function(){},1000)

但是,如果出现阻塞事件,定时器可能就要隔1000+ms才执行一次。要精确的实现每隔1s执行一次,必须要先获取阻塞的时间。这里要用到定时器函数setTimeout控制定时器的执行时间,代码实现如下

setInterval(function(){
let j = 0
while(j++ < 100000000){}
},1)
let interval = 1000,
ms = 50000, //从服务器和活动开始时间计算出的时间差,这里测试用50000ms
count = 0,
startTime = new Date().getTime();
if( ms >= 0){
var timeCounter = setTimeout(countDownStart,interval);
}

第一部分的setInterval是一段阻塞代码。然后,我们分别定义了interval作为定时器的执行时间,距活动结束的时间用ms表示(ms=活动结束时间-服务器时间),
count表示计数器,然后启动定时器timeCounter。其中,countDownStart函数的实现逻辑如下

function countDownStart(){
count++;
let offset = new Date().getTime() - (startTime + count * interval);
let nextTime = interval - offset;
if (nextTime < 0) { nextTime = 0 };
ms -= interval;
console.log("误差:" + offset + "ms" );
if(ms < 0){
clearTimeout(timeCounter);
}else{
timeCounter = setTimeout(countDownStart,nextTime);
}
}

countDownStart的实现原理是,首先定义一个变量offset用来记录阻塞导致延误的时间是多少。nextTime代表offset和interval的差距,根据nextTime修改定时器timeCounter的执行时间,使它nextTime(ms)执行一次。

打个比方,如果上一次执行过程中因为阻塞延误了100ms,那么下一次就提前100ms启动定时器,即

timeCounter = setTimeout(countDownStart,900)



原文链接:https://blog.csdn.net/weixin_41672178/article/details/88372553

收起阅读 »

webpack手写loader

手写loader   我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则: 单一原则: 每个Loader只做一件事,...
继续阅读 »

手写loader


  我们在在Webpack配置基础篇介绍过,loader是链式传递的,对文件资源从上一个loader传递到下一个,而loader的处理也遵循着从下到上的顺序,我们简单了解一下loader的开发原则:



  1. 单一原则: 每个Loader只做一件事,简单易用,便于维护;

  2. 链式调用: Webpack 会按顺序链式调用每个Loader;

  3. 统一原则: 遵循Webpack制定的设计规则和结构,输入与输出均为字符串,各个Loader完全独立,即插即用;

  4. 无状态原则:在转换不同模块时,不应该在loader中保留状态;


  因此我们就来尝试写一个less-loaderstyle-loader,将less文件处理后通过style标签的方式渲染到页面上去。


同步loader


  loader默认导出一个函数,接受匹配到的文件资源字符串和SourceMap,我们可以修改文件内容字符串后再返回给下一个loader进行处理,因此最简单的一个loader如下:


module.exports = function(source, map){
return source
}


导出的loader函数不能使用箭头函数,很多loader内部的属性和方法都需要通过this进行调用,比如this.cacheable()来进行缓存、this.sourceMap判断是否需要生成sourceMap等。



  我们在项目中创建一个loader文件夹,用来存放我们自己写的loader,然后新建我们自己的style-loader:


//loader/style-loader.js
function loader(source, map) {
let style = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style)
`
;
return style;
}
module.exports = loader;

  这里的source就可以看做是处理后的css文件字符串,我们把它通过style标签的形式插入到head中;同时我们也发现最后返回的是一个JS代码的字符串,webpack最后会将返回的字符串打包进模块中。


异步loader


  上面的style-loader都是同步操作,我们在处理source时,有时候会进行异步操作,一种方法是通过async/await,阻塞操作执行;另一种方法可以通过loader本身提供的回调函数callback


//loader/less-loader
const less = require("less");
function loader(source) {
const callback = this.async();
less.render(source, function (err, res) {
let { css } = res;
callback(null, css);
});
}
module.exports = loader;

  callback的详细传参方法如下:


callback({
//当无法转换原内容时,给 Webpack 返回一个 Error
error: Error | Null,
//转换后的内容
content: String | Buffer,
//转换后的内容得出原内容的Source Map(可选)
sourceMap?: SourceMap,
//原内容生成 AST语法树(可选)
abstractSyntaxTree?: AST
})

  有些时候,除了将原内容转换返回之外,还需要返回原内容对应的Source Map,比如我们转换less和scss代码,以及babel-loader转换ES6代码,为了方便调试,需要将Source Map也一起随着内容返回。


//loader/less-loader
const less = require("less");
function loader(source) {
const callback = this.async();
less.render(source,{sourceMap: {}}, function (err, res) {
let { css, map } = res;
callback(null, css, map);
});
}
module.exports = loader;

  这样我们在下一个loader就能接收到less-loader返回的sourceMap了,但是需要注意的是:



Source Map生成很耗时,通常在开发环境下才会生成Source Map,其它环境下不用生成。Webpack为loader提供了this.sourceMap这个属性来告诉loader当前构建环境用户是否需要生成Source Map。



加载本地loader


  loader文件准备好了之后,我们需要将它们加载到webpack配置中去;在基础篇中,我们加载第三方的loader只需要安装后在loader属性中写loader名称即可,现在加载本地loader需要把loader的路径配置上。


module.exports = {
module: {
rules: [{
test: /\.less/,
use: [
{
loader: './loader/style-loader.js',
},
{
loader: path.resolve(__dirname, "loader", "less-loader"),
},
],
}]
}
}

  我们可以在loader中配置本地loader的相对路径或者绝对路径,但是这样写起来比较繁琐,我们可以利用webpack提供的resolveLoader属性,来告诉webpack应该去哪里解析本地loader。


module.exports = {
module: {
rules: [{
test: /\.less/,
use: [
{
loader: 'style-loader',
},
{
loader: 'less-loader',
},
],
}]
},
resolveLoader:{
modules: [path.resolve(__dirname, 'loader'), 'node_modules']
}
}

  这样webpack会先去loader文件夹下找loader,没有找到才去node_modules;因此我们写的loader尽量不要和第三方loader重名,否则会导致第三方loader被覆盖加载。


处理参数


  我们在配置loader时,经常会给loader传递参数进行配置,一般是通过options属性来传递的,也有像url-loader通过字符串来传参:


{
test: /\.(jpg|png|gif|bmp|jpeg)$/,
use: 'url-loader?limt=1024&name=[hash:8].[ext]'
}

  webpack也提供了query属性来获取传参;但是query属性很不稳定,如果像上面的通过字符串来传参,query就返回字符串格式,通过options方式就会返回对象格式,这样不利于我们处理。因此我们借助一个官方的包loader-utils帮助处理,它还提供了很多有用的工具。


const { 
getOptions,
parseQuery,
stringifyRequest,
} = require("loader-utils");

module.exports = function (source, map) {
//获取options参数
const options = getOptions(this);
//解析字符串为对象
parseQuery("?param1=foo")
//将绝对路由转换成相对路径
//以便能在require或者import中使用以避免绝对路径
stringifyRequest(this, "test/lib/index.js")
}

  常用的就是getOptions将处理后的参数返回出来,它内部的实现逻辑也非常的简单,也是根据query属性进行处理,如果是字符串的话调用parseQuery方法进行解析,源码如下:


//loader-utils/lib/getOptions.js
'use strict';
const parseQuery = require('./parseQuery');
function getOptions(loaderContext) {
const query = loaderContext.query;
if (typeof query === 'string' && query !== '') {
return parseQuery(loaderContext.query);
}
if (!query || typeof query !== 'object') {
return {};
}
return query;
}
module.exports = getOptions;

  获取到参数后,我们还需要对获取到的options参数进行完整性校验,避免有些参数漏传,如果一个个判断校验比较繁琐,这就用到另一个官方包schema-utils


const { getOptions } = require("loader-utils");
const { validate } = require("schema-utils");
const schema = require("./schema.json");
module.exports = function (source, map) {
const options = getOptions(this);
const configuration = { name: "Loader Name"};
validate(schema, options, configuration);
//省略其他代码
}

  validate函数并没有返回值,打印返回值发现是`undefined,因为如果参数不通过的话直接会抛出ValidationError异常,直接进程中断;这里引入了一个schema.json,就是我们对options``中参数进行校验的一个json格式的对应表:


{
"type": "object",
"properties": {
"source": {
"type": "boolean"
},
"name": {
"type": "string"
},
},
"additionalProperties": false
}

  properties中的健名就是我们需要检验的options中的字段名称,additionalProperties代表了是否允许options中还有其他额外的属性。


less-loader源码分析


  写完我们自己简单的less-loader,让我们来看一下官方的less-loader源码到底是怎么样的,这里贴上部分源码:


import less from 'less';
import { getOptions } from 'loader-utils';
import { validate } from 'schema-utils';
import schema from './options.json';
async function lessLoader(source) {
const options = getOptions(this);
//校验参数
validate(schema, options, {
name: 'Less Loader',
baseDataPath: 'options',
});
const callback = this.async();
//对options进一步处理,生成less渲染的参数
const lessOptions = getLessOptions(this, options);
//是否使用sourceMap,默认取options中的参数
const useSourceMap =
typeof options.sourceMap === 'boolean'
? options.sourceMap : this.sourceMap;
//如果使用sourceMap,就在渲染参数加入
if (useSourceMap) {
lessOptions.sourceMap = {
outputSourceFiles: true,
};
}
let data = source;
let result;
try {
result = await less.render(data, lessOptions);
} catch (error) {
}
const { css, imports } = result;
//有sourceMap就进行处理
let map =
typeof result.map === 'string'
? JSON.parse(result.map) : result.map;

callback(null, css, map);
}
export default lessLoader;

  可以看到官方的less-loader和我们写的简单的loader本质上都是调用less.render函数,对文件资源字符串进行处理,然后将处理好后的字符串和sourceMap通过callback返回。


loader依赖


  在loader中,我们有时候也会使用到外部的资源文件,我们需要在loader对这些资源文件进行声明;这些声明信息主要用于使得缓存loader失效,以及在观察模式(watch mode)下重新编译。


  我们尝试写一个banner-loader,在每个js文件资源后面加上我们自定义的注释内容;如果传了filename,就从文件中获取预设好的banner内容,首先我们预设两个banner的txt:


//loader/banner1.txt
/* build from banner1 */

//loader/banner2.txt
/* build from banner2 */

  然后在我们的banner-loader中根据参数来进行判断:


//loader/banner-loader
const fs = require("fs");
const path = require("path");
const { getOptions } = require("loader-utils");

module.exports = function (source) {
const options = getOptions(this);
if (options.filename) {
let txt = "";
if (options.filename == "banner1") {
this.addDependency(path.resolve(__dirname, "./banner1.txt"));
txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
} else if (options.filename == "banner2") {
this.addDependency(path.resolve(__dirname, "./banner1.txt"));
txt = fs.readFileSync(path.resolve(__dirname, "./banner1.txt"));
}
return source + txt;
} else if (options.text) {
return source + `/* ${options.text} */`;
} else {
return source;
}
};

  这里使用了this.addDependency的API将当前处理的文件添加到文件依赖中(并不是项目的package.json)。如果在观察模式下,依赖的text文件发生了变化,那么打包生成的文件内容也随之变化。



如果不添加this.addDependency的话项目并不会报错,只是在观察模式下,如果依赖的文件发生了变化生成的bundle文件并不能及时更新。



缓存加速


  在有些情况下,loader处理需要大量的计算非常耗性能(比如babel-loader),如果每次构建都重新执行相同的转换操作每次构建都会非常慢。


  因此webpack默认会将loader的处理结果标记为可缓存,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时,它的输出结果必然是相同的;如果不想让webpack缓存该loader,可以禁用缓存:


module.exports = function(source) {
// 强制不缓存
this.cacheable(false);
return source;
};

手写loader所有代码均在webpackdemo19



收起阅读 »

深入webpack打包原理

本文讨论的核心内容如下: webpack进行打包的基本原理 如何自己实现一个loader和plugin 注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11,node版本是v12.14.1,npm版本v6.13....
继续阅读 »

本文讨论的核心内容如下:



  1. webpack进行打包的基本原理

  2. 如何自己实现一个loaderplugin


注: 本文使用的webpack版本是v4.43.0, webpack-cli版本是v3.3.11node版本是v12.14.1npm版本v6.13.4(如果你喜欢yarn也是可以的),演示用的chrome浏览器版本81.0.4044.129(正式版本) (64 位)


1. webpack打包基本原理


webpack的一个核心功能就是把我们写的模块化的代码,打包之后,生成可以在浏览器中运行的代码,我们这里也是从简单开始,一步步探索webpack的打包原理


1.1 一个简单的需求


我们首先建立一个空的项目,使用npm init -y快速初始化一个package.json,然后安装webpack webpack-cli


接下来,在根目录下创建src目录,src目录下创建index.jsadd.jsminus.js,根目录下创建index.html,其中index.html引入index.js,在index.js引入add.jsminus.js


目录结构如下:



文件内容如下:

// add.js
export default (a, b) => {
return a + b
}
// minus.js
export const minus = (a, b) => {
return a - b
}
// index.js
import add from './add.js'
import { minus } from './minus.js'

const sum = add(1, 2)
const division = minus(2, 1)
console.log('sum>>>>>', sum)
console.log('division>>>>>', division)
<!--index.html-->

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>demo</title>
</head>
<body>
<script src="./src/index.js"></script>
</body>
</html>

<<span class="hljs-attribute">span</span> class=<span class="hljs-string">"hljs-attribute"</span>>demo</span>

这样直接在index.html引入index.js的代码,在浏览器中显然是不能运行的,你会看到这样的错误


Uncaught SyntaxError: Cannot use import statement outside a module

是的,我们不能在script引入的js文件里,使用es6模块化语法


1.2 实现webpack打包核心功能


我们首先在项目根目录下再建立一个bundle.js,这个文件用来对我们刚刚写的模块化js代码文件进行打包


我们首先来看webpack官网对于其打包流程的描述:


it internally builds a dependency graph which maps every module your project needs and generates one or more bundles(webpack会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个 bundle)


在正式开始之前,结合上面webpack官网说明进行分析,明确我们进行打包工作的基本流程如下:



  1. 首先,我们需要读到入口文件里的内容(也就是index.js的内容)

  2. 其次,分析入口文件,递归的去读取模块所依赖的文件内容,生成依赖图

  3. 最后,根据依赖图,生成浏览器能够运行的最终代码


1. 处理单个模块(以入口为例)


1.1 获取模块内容


既然要读取文件内容,我们需要用到node.js的核心模块fs,我们首先来看读到的内容是什么:

// bundle.js
const fs = require('fs')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
}
getModuleInfo('./src/index.js')


1.2 分析模块内容


我们安装@babel/parser,演示时安装的版本号为^7.9.6


这个babel模块的作用,就是把我们js文件的代码内容,转换成js对象的形式,这种形式的js对象,称做抽象语法树(Abstract Syntax Tree, 以下简称AST)

// bundle.js
const fs = require('fs')
const parser = require('@babel/parser')
const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
// 表示我们要解析的是es6模块
sourceType: 'module'
})
console.log(ast)
console.log(ast.program.body)
}
getModuleInfo('./src/index.js')



入口文件内容被放到一个数组中,总共有六个Node节点,我们可以看到,每个节点有一个type属性,其中前两个的type属性是ImportDeclaration,这对应了我们入口文件的两条import语句,并且,每一个type属性是ImportDeclaration的节点,其source.value属性是引入这个模块的相对路径,这样我们就得到了入口文件中对打包有用的重要信息了。


接下来要对得到的ast做处理,返回一份结构化的数据,方便后续使用。


1.3 对模块内容做处理


ast.program.body部分数据的获取和处理,本质上就是对这个数组的遍历,在循环中做数据处理,这里同样引入一个babel的模块@babel/traverse来完成这项工作。


安装@babel/traverse,演示时安装的版本号为^7.9.6

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
console.log(deps)
}
getModuleInfo('./src/index.js')

创建一个对象deps,用来收集模块自身引入的依赖,使用traverse遍历ast,我们只需要对ImportDeclaration的节点做处理,注意我们做的处理实际上就是把相对路径转化为绝对路径,这里我使用的是Mac系统,如果是windows系统,注意斜杠的区别


获取依赖之后,我们需要对ast做语法转换,把es6的语法转化为es5的语法,使用babel核心模块@babel/core以及@babel/preset-env完成


安装@babel/core @babel/preset-env,演示时安装的版本号均为^7.9.6

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module'
})
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
console.log(moduleInfo)
return moduleInfo
}
getModuleInfo('./src/index.js')


2. 递归的获取所有模块的信息

这个过程,也就是获取依赖图(dependency graph)的过程,这个过程就是从入口模块开始,对每个模块以及模块的依赖模块都调用getModuleInfo方法就行分析,最终返回一个包含所有模块信息的对象

const parseModules = file => {
// 定义依赖图
const depsGraph = {}
// 首先获取入口的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍历模块的依赖,递归获取模块信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
console.log(depsGraph)
return depsGraph
}
parseModules('./src/index.js')


3. 生成最终代码


在我们实现之前,观察上一节最终得到的依赖图,可以看到,最终的code里包含exports以及require这样的语法,所以,我们在生成最终代码时,要对exports和require做一定的实现和处理


我们首先调用之前说的parseModules方法,获得整个应用的依赖图对象:


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
}

接下来我们应该把依赖图对象中的内容,转换成能够执行的代码,以字符串形式输出。
我们把整个代码放在自执行函数中,参数是依赖图对象


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
return exports
}
require('${file}')
})(${depsGraph})`
}

接下来内容其实很简单,就是我们取得入口文件的code信息,去执行它就好了,使用eval函数执行,初步写出代码如下:


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
(function(code){
eval(code)
})(graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}

上面的写法是有问题的,我们需要对file做绝对路径转化,否则graph[file].code是获取不到的,定义adsRequire方法做相对路径转化为绝对路径


const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}

接下来,我们只需要执行bundle方法,然后把生成的内容写入一个JavaScript文件即可


const content = bundle('./src/index.js')
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)

4. bundle.js的完整代码

const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const getModuleInfo = file => {
const body = fs.readFileSync(file, 'utf-8')
console.log(body)
const ast = parser.parse(body, {
sourceType: 'module'
})
// console.log(ast.program.body)
const deps = {}
traverse(ast, {
ImportDeclaration({ node }) {
const dirname = path.dirname(file);
const absPath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = absPath
}
})
const { code } = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"]
})
const moduleInfo = { file, deps, code }
return moduleInfo
}

const parseModules = file => {
// 定义依赖图
const depsGraph = {}
// 首先获取入口的信息
const entry = getModuleInfo(file)
const temp = [entry]
for (let i = 0; i < temp.length; i++) {
const item = temp[i]
const deps = item.deps
if (deps) {
// 遍历模块的依赖,递归获取模块信息
for (const key in deps) {
if (deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
// console.log(depsGraph)
return depsGraph
}


// 生成最终可以在浏览器运行的代码
const bundle = file => {
const depsGraph = JSON.stringify(parseModules(file))
return `(function(graph){
function require(file) {
var exports = {};
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports
}
require('${file}')
})(${depsGraph})`
}


const build = file => {
const content = bundle(file)
// 写入到dist/bundle.js
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
}

build('./src/index.js')



收起阅读 »

关于 webpack 的几个知识点

随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,比如 sass 和 less 的代码浏览器是不支持的, 但如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack 脱颖而出成为了当今...
继续阅读 »

随着现代前端开发的复杂度和规模越来越庞大,已经不能抛开工程化来独立开发了,比如 sass 和 less 的代码浏览器是不支持的, 但如果摒弃了这些开发框架,那么开发的效率将大幅下降。在众多前端工程化工具中,webpack 脱颖而出成为了当今最流行的前端构建工具。 然而大多数的使用者都只是单纯的会使用,而并不知道其深层的原理。希望通过以下的面试题总结可以帮助大家温故知新、查缺补漏,知其然而又知其所以然。

1. webpack 与 grunt、gulp 的不同?

三者都是前端构建工具,grunt 和 gulp 在早期比较流行,现在 webpack 相对来说比较主流,不过一些轻量化的任务还是会用 gulp 来处理,比如单独打包 CSS 文件等。

grunt 和 gulp 是基于任务和流(Task、Stream)的。类似 jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个 web 的构建流程。

webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能。

所以总结一下:

  • 从构建思路来说
    • gulp 和 grunt 需要开发者将整个前端构建过程拆分成多个 `Task`,并合理控制所有 `Task` 的调用关系。
      webpack 需要开发者找到入口,并需要清楚对于不同的资源应该使用什么 Loader 做何种解析和加工。
  • 对于知识背景来说
    • gulp 更像后端开发者的思路,需要对于整个流程了如指掌。 webpack 更倾向于前端开发者的思路。

2. 你为什么最终选择使用 webpack?

基于入口的打包工具除了 webpack 以外,主流的还有:rollup 和 parcel

从应用场景上来看:

  • webpack 适用于大型复杂的前端站点构建
  • rollup 适用于基础库的打包,如 vue、react
  • parcel 适用于简单的实验性项目,他可以满足低门槛的快速看到效果

由于 parcel 在打包过程中给出的调试信息十分有限,所以一旦打包出错难以调试,所以不建议复杂的项目使用 parcel

3. 有哪些常见的 Loader?解决什么问题?

  • babel-loader:把 ES6 转换成 ES5
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
  • image-loader:加载并且压缩图片文件
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件
  • url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去
  • source-map-loader:加载额外的 Source Map 文件,以方便断点调试

4. 有哪些常见的Plugin?解决什么问题?

  • define-plugin:定义环境变量
  • commons-chunk-plugin:提取公共代码
  • uglifyjs-webpack-plugin:通过 UglifyES 压缩 ES6 代码
  • HTMLWebpackPlugin:webpack 在自定生成 html 时需要用到它,能自动引入 js/css 文件
  • MiniCssExtractPlugin:将 css 代码抽成单独的文件,一般适用于发布环境,生产环境用 css-loader

5. Loader 和 Plugin 的不同?

不同的作用

  • Loader 直译为"加载器"。webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件,如果想将其他文件也打包的话,就会用到 loader。 所以 Loader 的作用是让 webpack 拥有了加载和解析非JavaScript文件 的能力。
  • Plugin 直译为"插件"。Plugin 可以扩展 webpack 的功能,让 webpack 具有更多的灵活性。 在 webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

不同的用法

  • Loader 在 module.rules 中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载器 (loader) 和使用的参数(options
  • Plugin 在 plugins 中单独配置。 类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入。

6. 如何利用 webpack 来优化前端性能?

用 webpack 优化前端性能是指:优化 webpack 的输出结果,让打包的最终结果在浏览器运行快速高效。

  • 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用 webpack 的 UglifyJsPluginParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩 css
  • 利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用 webpack 对于output参数和各loader 的 publicPath 参数来修改资源路径
  • 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现
  • 提取公共代码。

7. 如何提高 webpack 的构建速度?

  1. 多入口情况下,使用 CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常用库
  3. 利用 DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过 DllReferencePlugin将预编译的模块加载进来。
  4. 使用 Happypack 实现多线程加速编译
  5. 使用 webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采用了多核并行压缩来提升压缩速度
  6. 使用 Tree-shaking 和 Scope Hoisting 来剔除多余代码
原文:https://blog.csdn.net/Marker__/article/details/107619259
收起阅读 »

关于webpack面试题总结

最近在读《webpack深入浅出》,总结一下webpack关于面试常见的问题,分享出来,希望可以帮助更多小伙伴在找到心爱的工作和期待的薪水。一.常见的构建工具有哪些?他们各自优缺点?为什么选择了webpack?Grunt、Gulp、Fis3、Rollup、Np...
继续阅读 »

最近在读《webpack深入浅出》,总结一下webpack关于面试常见的问题,分享出来,希望可以帮助更多小伙伴在找到心爱的工作和期待的薪水。

一.常见的构建工具有哪些?他们各自优缺点?为什么选择了webpack?

Grunt、Gulp、Fis3、Rollup、Npm Script、webpack

<1>Grunt的优点是:

• 灵活,它只负责执行我们定义的任务;

• 大量的可复用插件封装好了常见的构建任务。

Grunt的缺点是:

集成度不高,要写很多配置后才可以用,无法做到开箱即用。Grunt相当于进化版的NpmScript,它的诞生其实是为了弥补NpmScript的不足。

<2>Gulp的优点是: 好用又不失灵活,既可以单独完成构建,也可以和其他工具搭配使用。

其缺点: 和Grunt类似,集成度不高,要写很多配置后才可以用,无法做到开箱即用。

<3> Fis3的优点是:集成了各种Web开发所需的构建功能,配置简单、开箱即用。

其缺点是 目前官方己经不再更新和维护,不支持最新版本的T、fode

<4>Webpack的优点是:• 专注于处理模块化的项目,能做到开箱即用、一步到位:

• 可通过Plugin扩展,完整好用又不失灵活;

• 使用场景不局限于Web开发

• 社区庞大活跃,经常引入紧跟时代发展的新特性,能为大多数场景找到已有的开源扩展:

• 良好的开发体验。

Webpack的缺点是:只能用于采用模块化开发的项目。

<5> Rollup是在Webpack流行后出现的替代品,讲述差别::
• Rollup生态链还不完善,体验不如Webpack;

• Rollup的功能不如Webpack完善,但其配置和使用更简单:

• Rollup不支持CodeSpliting,但好处是在打包出来的代码中没有Webpack那段模块的加载、执行和缓存的代码。

Roll up在用于打包JavaScript库时比Webpack更有优势,因为其打包出来的代码更小、

深入浅出Webpack更快。

缺点:但它的功能不够完善,在很多场景下都找不到现成的解决方案

<6>Npm Script的优点 是内置,无须安装其他依赖。
其缺点 是功能太简单,虽然提供了pre和post两个钩子,但不能方便地管理多个任务之间的依赖

为啥选择webpack?
大多数团队在开发新项目时会采用紧跟时代的技术,这些技术几乎都会采用“模块化+新语言+新框架”,Webpack可以为这些新项目提供一站式的解决方案:
• Webpack有良好的生态链和维护团队,能提供良好的开发体验并保证质量:

• Webpack被全世界大量的Web开发者使用和验证,能找到各个层面所需的教程和经验分享。

二.有哪些常见的Loader?你用过哪些Loader?

1. 加载文件
• raw-loader :将文本文件的内容加载到代码中

• file-loader :将文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件

• url-loader :和 file-loader 类似,但是能在文件很小的情况下以 base64 方式将文件的内容注入代码中

• source-map-loader :加载额外的 SourceMap 文件,以方便断点调试

• svg-inline-loader :将压缩后的SVG 内容注入代码中

• node-loader :加载 Node.js 原生模块的 .node 文件

• image-loader :加载并且压缩图片文件

• json-loader:加载 JSON 文件

• yaml-loader:加载 YAML 文件

2. 编译模版
• pug-loader :将 Pug 模版转换成 JavaScript 函数井返回。

• handlebars-loader:将 Handlebars模版编译成函数并返回

• s-loader :将 EJS 模版编译成函数井返回

• haml-loader:将 HAML 代码转换成 HTML

• markdown-loader 将 Markdown 文件转换成 HTML

3.转换脚本语言
• babel-loader :将 ES6 转换成 ES5

• ts-loader :将 TypeScript 转换成 JavaScript,

• awesome-typescript-loader: Type Script 转换成 JavaScript ,性能要比 ts-loader好

• coffee-loader 将 CoffeeScript换成 JavaScript

4.转换样式文件
• css-loader :加载 css ,支持模块化、压缩、文件导入等特性。

• style-loader :将 css 代码 注入JavaScript 中,通过 DOM 操作去加载 css

• sass-loader :将 SCSS SASS 代码转换成 css

• postcss-loader : 扩展 css 语法,使用css

• less-loader : Less 代码转换成 css代码

• stylus-loader :将 Stylu 代码转换成 css 码。

5. 检查代码
• eslint-loader :通过 ESLint 检查 JavaScript
代码

• tslint-loader :通过 TSLint peScript
代码

• mocha-loader :加载 Mocha 测试
用例的代码

• coverjs-loader : 计算测试的覆盖率。

6.其他 Loader
• vue-loader :加载 Vue. 单文件组件

• i18n-loader:加载多语言版本,支持国际化

• ignore-loader :忽略部分文件

• ui-component-loader:按需加载
UI 组件库,例如在使用 antdUI 组件库时,不会因为只用到了 Button 组件而打包进所有的组件

3.有哪些常见的Plugin?你用过哪些Plugin?

1.用于修改行为
• define-plugin :定义环境变量

• context-replacement-plugin : 修改 require 语句在寻找文件时的默认行为

• ignore-plugin :用 于忽略部分文件

2.用于优化
• commons-chunk-plugin :提取公共代码。

• extract-text-webpack-plugin :提取 JavaScript 中的 css 代码到单独的文件中

• prepack-webpack-plugin :通过Facebook Prepack 优化输出的 JavaScript 代码的性能

• uglifyjs-webpack-plugin :通过 UglifyES 压缩 S6 代码

• webpack-parallel-uglify-plugin :多进程执行 glifyJS 代码压缩,提升构建的速度

• imagemin-webpack-plugin : 压缩图片文件。

• webpack-spritesmith :用插件制作碧图

• ModuleConcatenationPlugin : 开启 WebpackScopeHoisting 功能

• dll-plugin :借鉴 DDL 的思想大幅度提升构建速度

• hot-module-replacem nt-plugin 开启模块热替换功能。

3. 其他 Plugin
• serviceworker-webpack-plugin :为网页应用增加离钱缓存功能

• stylelint-webpack-plugin : stylelint集成到项目中,

• i18n-webpack-plugin : 使网页支持国际化。

• provide-plugin : 从环境中提供的全局变量中加载模块,而不用导入对应的文件。

• web-webpack-plugin : 可方便地为单页应用输出 HTML ,比 html-webpack-plugin 好用

4.那你再说一说Loader和Plugin的区别

Loader :模块转换器,用于将模块的原内容按照需求转换成新内容。
Plugin :扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑,来改变构建结
果或做我们想要的事情。

5.Webpack构建流程简单说一下

初始化参数 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
• 开始编译:用上 步得到的参数初始 Co er 对象,加载所有配置的插件,通
过执行对象的 run 方法开始执行编译
• 确定入口 根据配置中的 ntry 找出所有入口文件
• 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出
模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
• 完成模块编译 在经过第 步使用 Loader 翻译完所有模块后, 得到了每个模块被
翻译后的最终内容及它们之间的依赖关系。
• 输出资源:根据入口和模块之间的依赖关系,组装成 个个包含多个模块的 Chunk,
再将每个 Chunk 转换成 个单独的文件加入输出列表中,这是可以修改输出内容
的最后机会
• 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内
容写入文件系统中。

6.使用webpack开发时,你用过哪些可以提高效率的插件

webpack-dashboard:可以更友好的展示相关打包信息。
webpack-merge:提取公共配置,减少重复配置代码
speed-measure-webpack-plugin:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。
size-plugin:监控资源体积变化,尽早发现问题
HotModuleReplacementPlugin:模块热替换

7.模块打包原理知道吗?


8.什么 是模块热更新?


devServer.hot 配置是否启用 ,开启模块热替换功能后,将在不刷新整个页面的情况下通过用新模块替换老模块来做到实时预览


9.如何提高webpack的构建速度?


10.文件监听原理呢?


11.source map是什么?生产环境怎么用?


12.如何对bundle体积进行监控和分析?


13.文件指纹是什么?怎么用?


14.在实际工程中,配置文件上百行乃是常事,如何保证各个loader按照预想方式工作?


15.如何优化 Webpack 的构建速度?


16.你刚才也提到了代码分割,那代码分割的本质是什么?有什么意义呢?


17.是否写过Loader?简单描述一下编写loader的思路?


18.是否写过Plugin?简单描述一下编写Plugin的思路?


19.聊一聊Babel原理吧?


20.什么是Tree-shaking?
Tree Shaking 可以用来剔除 JavaScript 中用 不上的死代码。


21.如何实现 按需加载?


``import(/* webpackChunkName : ” show " */ ’. / show ’>


Webpack 内置了对 import *)语句的支持,当 Wepack 遇到了类似的语句时会这样


处理:
• 以./ show.j 为入口重新生成一个 Chunk;
• 代码执行到 import 所在的语句时才去加载由 Chunk 对应生成的文件:
• import 返回一个 Promise ,当文件加载成功时可以在 Promise then 方法中获取
show.j 导出的内容。``


22.如何配置单页应用?如何配置多页应用?


23.如何利用webpack来优化前端性能?(提高性能和体验)


24.npm打包时需要注意哪些?如何利用webpack来更好的构建


25.什么是模块化,都有哪些?


模块化是指一个复杂的系统分解为多个模块以方便编码。


js模块化:


mommon.js:核型思想,通过require方法来同步加载依赖的其他模块,通过module.exports导出需要暴露的接口。


优点
1.代码可复用于node环境并运行,例如同构应用
2.通过npm发布的很多第三方模块都采用了mommonJS规范


缺点:1.无法直接运行在浏览器环境下,必需通过工具转换成标准的es5


AMD:异步方式去加载依赖的模块,主要用来解决针对浏览器环境的模块化问题,最具代表的实现是require.js


优点
1.可在不转换代码的情况下,直接在浏览器中运行
2.可异步加载依赖
3.可并行加载多个依赖
4.代码可运行在浏览器和node环境下


缺点 :1.js运行环境没有原生支持AMD,需要先导入实现了AMD的库后才能正常使用。


es6模块化:

import { readFile} from 'fs';
import react from 'react';

// 导出
export function hello(){};
export default{...}


链接:https://juejin.cn/post/6855129007856451591

收起阅读 »

NodeJs中的stream(流)- 基础篇

一、什么是Stream(流) 流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。 流是可读的、可写的,或...
继续阅读 »

一、什么是Stream(流)



流(stream)在 Node.js 中是处理流数据的抽象接口(abstract interface)。 stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。



流是可读的、可写的,或是可读写的。


二、NodeJs中的Stream的几种类型


Node.js 中有四种基本的流类型:



  • Readable - 可读的流(fs.createReadStream())

  • Writable - 可写的流(fs.createWriteStream())

  • Duplex - 可读写的流(net.Socket)

  • Transform - 在读写过程中可以修改和变换数据的 Duplex 流 (例如 zlib.createDeflate())


NodeJs中关于流的操作被封装到了Stream模块中,这个模块也被多个核心模块所引用。

const stream = require('stream');

在 NodeJS 中对文件的处理多数使用流来完成



  • 普通文件

  • 设备文件(stdin、stdout)

  • 网络文件(http、net)


注:在NodeJs中所有的Stream(流)都是EventEmitter的实例


Example:


1.将1.txt的文件内容读取为流数据

const fs = require('fs');

// 创建一个可读流(生产者)
let rs = fs.createReadStream('./1.txt');

通过fs模块提供的createReadStream()可以轻松创建一个可读的文件流。但我们并有直接使用Stream模块,因为fs模块内部已经引用了Stream模块并做了封装。所以说 流(stream)在 Node.js 中是处理流数据的抽象接口,提供了基础Api来构建实现流接口的对象。

var rs = fs.createReadStream(path,[options]);

1.path 读取文件的路径


2.options



  • flags打开文件的操作, 默认为'r'

  • mode 权限位 0o666

  • encoding默认为null

  • start开始读取的索引位置

  • end结束读取的索引位置(包括结束位置)

  • highWaterMark读取缓存区默认的大小64kb


Node.js 提供了多种流对象。 例如:



  • HTTP 请求 (request response)

  • process.stdout 就都是流的实例。


2.创建可写流(消费者)处理可读流


将1.txt的可读流 写入到2.txt文件中 这时我们需要一个可写流

const fs = require('fs');
// 创建一个可写流
let ws = fs.createWriteStream('./2.txt');
// 通过pipe让可读流流入到可写流 写入文件
rs.pipe(ws);
var ws = fs.createWriteStream(path,[options]);

1.path 读取文件的路径


2.options



  • flags打开文件的操作, 默认为'w'

  • mode 权限位 0o666

  • encoding默认为utf8

  • autoClose:true是否自动关闭文件

  • highWaterMark读取缓存区默认的大小16kb


pipe 它是Readable流的方法,相当于一个"管道",数据必须从上游 pipe 到下游,也就是从一个 readable 流 pipe 到 writable 流。

后续将深入将介绍pipe。




如上图,我们把文件比作装水的桶,而水就是文件里的内容,我们用一根管子(pipe)连接两个桶使得水从一个桶流入另一个桶,这样就慢慢的实现了大文件的传输过程。

三、为什么应该使用 Stream


当有用户在线看视频,假定我们通过HTTP请求返回给用户视频内容

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
fs.readFile(videoPath, (err, data) => {
res.end(data);
});
}).listen(8080);

但这样有两个明显的问题


1.视频文件需要全部读取完,才能返回给用户,这样等待时间会很长

2.视频文件一次全放入内存中,内存吃不消


用流可以将视频文件一点一点读到内存中,再一点一点返回给用户,读一部分,写一部分。(利用了 HTTP 协议的 Transfer-Encoding: chunked 分段传输特性),用户体验得到优化,同时对内存的开销明显下降

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
fs.createReadStream(videoPath).pipe(res);
}).listen(8080);

四、可读流(Readable Stream)



可读流(Readable streams)是对提供数据的源头(source)的抽象。



例如:



  • HTTP responses, on the client

  • HTTP requests, on the server

  • fs read streams

  • TCP sockets

  • process.stdin


所有的 Readable 都实现了 stream.Readable 类定义的接口。


可读流的两种模式(flowing 和 paused)


1.在 flowing 模式下, 可读流自动从系统底层读取数据,并通过 EventEmitter 接口的事件尽快将数据提供给应用。


2.在 paused 模式下,必须显式调用 stream.read()方法来从流中读取数据片段。


所有初始工作模式为paused的Readable流,可以通过下面三种途径切换为flowing模式:



  • 监听'data'事件

  • 调用stream.resume()方法

  • 调用stream.pipe()方法将数据发送到Writable


流动模式flowing


流切换到流动模式 监听data事件

const rs = fs.createReadStream('./1.txt');
const ws = fs.createWriteStream('./2.txt');
rs.on('data', chunk => {
ws.write(chunk);
});
ws.on('end', () => {
ws.end();
});

如果写入的速度跟不上读取的速度,有可能导致数据丢失。正常的情况应该是,写完一段,再读取下一段,如果没有写完的话,就让读取流先暂停,等写完再继续。

var fs = require('fs');
// 读取highWaterMark(3字节)数据,读完之后填充缓存区,然后触发data事件
var rs = fs.createReadStream(sourcePath, {
highWaterMark: 3
});
var ws = fs.createWriteStream(destPath, {
highWaterMark: 3
});

rs.on('data', function(chunk) { // 当有数据流出时,写入数据
if (ws.write(chunk) === false) { // 如果没有写完,暂停读取流
rs.pause();
}
});

ws.on('drain', function() { // 缓冲区清空触发drain事件 这时再继续读取
rs.resume();
});

rs.on('end', function() { // 当没有数据时,关闭数据流
ws.end();
});

或者使用更直接的pipe

fs.createReadStream(sourcePath).pipe(fs.createWriteStream(destPath));

暂停模式paused


1.在流没有 pipe() 时,调用 pause() 方法可以将流暂停

2.pipe() 时,需要移除所有 data 事件的监听,再调用 unpipe() 方法


read(size)

流在暂停模式下需要程序显式调用 read() 方法才能得到数据。read() 方法会从内部缓冲区中拉取并返回若干数据,当没有更多可用数据时,会返回null。read()不会触发'data'事件。


使用 read() 方法读取数据时,如果传入了 size 参数,那么它会返回指定字节的数据;当指定的size字节不可用时,则返回null。如果没有指定size参数,那么会返回内部缓冲区中的所有数据。

NodeJS 为我们提供了一个 readable 的事件,事件在可读流准备好数据的时候触发,也就是先监听这个事件,收到通知又数据了我们再去读取就好了:

const fs = require('fs');
rs = fs.createReadStream(sourcePath);

// 当你监听 readable事件的时候,会进入暂停模式
rs.on('readable', () => {
console.log(rs._readableState.length);
// read如果不加参数表示读取整个缓存区数据
// 读取一个字段,如果可读流发现你要读的字节小于等于缓存字节大小,则直接返回
let ch = rs.read(1);
});

暂停模式 缓存区的数据以链表的形式保存在BufferList中


五、可写流(Writable Stream)



可写流是对数据流向设备的抽象,用来消费上游流过来的数据,通过可写流程序可以把数据写入设备,常见的是本地磁盘文件或者 TCP、HTTP 等网络响应。



Writable 的例子包括了:



  • HTTP requests, on the client

  • HTTP responses, on the server

  • fs write streams

  • zlib streams

  • crypto streams

  • TCP sockets

  • child process stdin

  • process.stdout, process.stderr


所有 Writable 流都实现了 stream.Writable 类定义的接口。

process.stdin.pipe(process.stdout);

process.stdout 是一个可写流,程序把可读流 process.stdin 传过来的数据写入的标准输出设备。在了解了可读流的基础上理解可写流非常简单,流就是有方向的数据,其中可读流是数据源,可写流是目的地,中间的管道环节是双向流。


可写流使用


调用可写流实例的 write() 方法就可以把数据写入可写流

const fs = require('fs');
const rs = fs.createReadStream(sourcePath);
const ws = fs.createWriteStream(destPath);

rs.setEncoding('utf-8'); // 设置编码格式
rs.on('data', chunk => {
ws.write(chunk); // 写入数据
});

监听了可读流的 data 事件就会使可读流进入流动模式,我们在回调事件里调用了可写流的 write() 方法,这样数据就被写入了可写流抽象的设备destPath中。


write() 方法有三个参数



  • chunk {String| Buffer},表示要写入的数据

  • encoding 当写入的数据是字符串的时候可以设置编码

  • callback 数据被写入之后的回调函数


'drain'事件


如果调用 stream.write(chunk) 方法返回 false,表示当前缓存区已满,流将在适当的时机(缓存区清空后)触发 'drain

const fs = require('fs');
const rs = fs.createReadStream(sourcePath);
const ws = fs.createWriteStream(destPath);

rs.setEncoding('utf-8'); // 设置编码格式
rs.on('data', chunk => {
let flag = ws.write(chunk); // 写入数据
if (!flag) { // 如果缓存区已满暂停读取
rs.pause();
}
});

ws.on('drain', () => {
rs.resume(); // 缓存区已清空 继续读取写入
});

六、总结


stream(流)分为可读流(flowing mode 和 paused mode)、可写流、可读写流,Node.js 提供了多种流对象。 例如, HTTP 请求 和 process.stdout 就都是流的实例。stream 模块提供了基础的 API 。使用这些 API 可以很容易地来构建实现流接口的对象。它们底层都调用了stream模块并进行封装。



后续我们将继续对stream深入解析以及Readable Writable pipe的实现

作者:Brolly
链接:https://www.jianshu.com/p/1d36648fb87e
来源:简书

收起阅读 »

Bootstrap Table

前端1.BootStrap Table1.1.1 HTML<div> <div class="panel-body table-responsive"> <table id="productTable" class="tab...
继续阅读 »

前端

1.BootStrap Table

1.1.1 HTML

<div>
<div class="panel-body table-responsive">
<table id="productTable" class="table">
</table>
</div>
</div>

1.1.2 js初始化(开发常用方法)

$('#productTable').bootstrapTable('refreshOptions',{pageNumber:1,pageSize:10});
var tableObject= $.find("#productTable");
$(tableObject).bootstrapTable({
locale: 'zn-CN',
pageSize: 10,
pageNumber: 1,
pageList: [10, 25, 50,100],
clickToSelect: true,
striped: true,
ajax: function(ajaxParams) {
json.NEXT_KEY = (ajaxParams.data.offset /ajaxParams.data.limit + 1) + "";
json.PAGE_SIZE = ajaxParams.data.limit + "";
//json.SORT_NAME = ajaxParams.data.sort;
//json.SORT_ORDER = ajaxParams.data.order;
YT.ajaxData({
url:dataUrl,
params: json,
success: function (msg) {
var resultData = {total: msg.TOTAL_NUM||0,rows: msg.LIST|| []};
ajaxParams.success(resultData);
}
});
},
pagination: true,
sidePagination: 'server',
//sortName: '表格头排序字段',
//sortOrder: 'desc',
formatNoMatches: function() {
return "暂无数据";
},
columns: [
{
checkbox: true,
singleSelect : true,
align: 'center'
},
{
field: '',
title: '操作',
formatter: removeHtml,
align: 'center'
}]
});
// 自定义table列
function removeHtml(value,row,index){
var data = $("#productTable").bootstrapTable('getData');
var params= data[index];
return [
'<a class="btn btn-xs btn-primary" >自定义一些方法</a>'
].join('')
}
// 常用方法
1.获取当前table初始化数据
var data = $("#productTable").bootstrapTable('getData');
data-index:该属性是bootstrap table 下角标
2.获取多选选中行的数据
var data = $("#productTable").bootstrapTable('getSelections');
3.清楚多选框全选
$("#prodTable input[type='checkbox']:checked").prop("checked",false);
4.获取每页显示的数量
var pageSize = $('#prodTable').bootstrapTable('getOptions').pageSize;
5.获取当前是第几页
var pageNumber = $('#prodTable').bootstrapTable('getOptions').pageNumber;
6.隐藏列、显示列(可用于初始化table之后的列的动添显示与隐藏,执行该时间之后数据会回滚到初始化table时的数据)
$("#prodTable").bootstrapTable("hideColumn","GROUP_LEADER_PRICE")
$("#prodTable").bootstrapTable("showColumn","GROUP_LEADER_PRICE")

1.1.3 总计

function statisticsTableInit() {
var columns = [
{
field: 'column1',
title: '表头1',
align: 'center'
},
{
field: 'column2',
title: '表头2',
align: 'center'
},
{
field: 'column3',
title: '表头3',
align: 'center'
}
];
pageList.find("#prodTable").bootstrapTable({
locale: 'zn-CN',
columns: columns
});
}
function statisticsAjax(json) {
YT.ajaxData({
url:YT.dataUrl,
params: json,
success: function (msg) {
if(msg && msg.LIST){
pageList.find("#prodTable").bootstrapTable('load',(msg.LIST));
}
}
});
}


收起阅读 »

JavaScript重构技巧 — 函数和类

JavaScript 是一种易于学习的编程语言,编写运行并执行某些操作的程序很容易。然而,要编写一段干净的JavaScript 代码是很困难的。在本文中,我们将介绍一些与清理 JavaScript 函数和类有关的重构思想。不要直接对参数赋值在使用参数之前,我们...
继续阅读 »

JavaScript 是一种易于学习的编程语言,编写运行并执行某些操作的程序很容易。然而,要编写一段干净的JavaScript 代码是很困难的。

在本文中,我们将介绍一些与清理 JavaScript 函数和类有关的重构思想。

不要直接对参数赋值

在使用参数之前,我们应该删除对参数的赋值,并将参数值赋给变量。

例如,我们可能会写这样的代码:

const discount = (subtotal) => {
if (subtotal > 50) {
subtotal *= 0.8;
}
}

对比上面的代码,我们可以这样写:

const discount = (subtotal) => {
let _subtotal = subtotal;
if (_subtotal > 50) {
_subtotal *= 0.8;
}
}

因为参数有可能是通过值或者引用传递的,如果是引用传递的,直接负值操作,有些结果会让感到困惑。

本例是通过值传递的,但为了清晰起见,我们还是将参数赋值给变量了。

用函数替换方法

我们可以将一个方法变成自己的函数,以便所有类都可以访问它。

例如,我们可能会写这样的代码:

const hello = () => {
console.log('hello');
}
class Foo {
hello() {
console.log('hello');
}
//...
}
class Bar {
hello() {
console.log('hello');
}
//...
}

我们可以将hello方法提取到函数中,如下所示:

const hello = () => {
console.log('hello');
}
class Foo {
//...
}
class Bar {
//...
}

由于hello方法不依赖于this,并且在两个类中都重复,因此我们应将其移至其自己的函数中以避免重复。

替代算法

相对流程式的写法,我们想用一个更清晰的算法来代替,例如,我们可能会写这样的代码:

const doubleAll = (arr) => {
const results = []
for (const a of arr) {
results.push(a * 2);
}
return results;
}

对比上面的代码,我们可以这样写:

const doubleAll = (arr) => {
return arr.map(a => a * 2);
}

通过数组方法替换循环,这样doubleAll函数就会更加简洁。

如果有一种更简单的方法来解决我们的需求,那么我们就应该使用它。

移动方法

在两个类之间,我们可以把其中一个类的方法移动到另一个类中,例如,我们可能会写这样的代码:

class Foo {
method() {}
}
class Bar {
}

假如,我们在 Bar 类使用 method 的次数更多,那么应该把 method 方法移动到 Bar 类中, Foo 如果需要在直接调用 Bar 类的中方法即可。

class Foo {
}
class Bar {
method() {}
}

移动字段

除了移动方法外,我们还可以移动字段。例如,我们可能会写这样的代码:

class Foo {
constructor(foo) {
this.foo = foo;
}
}
class Bar {
}

跟移动方法的原因类似,我们有时这么改代码:

class Foo {
}
class Bar {
constructor(foo) {
this.foo = foo;
}
}

我们可以将字段移至最需要的地方

提取类

如果我们的类很复杂并且有多个方法,那么我们可以将额外的方法移到新类中。

例如,我们可能会写这样的代码:

class Person {
constructor(name, phoneNumber) {
this.name = name;
this.phoneNumber = phoneNumber;
}
addAreaCode(areaCode) {
return `${areaCode}-${this.phoneNumber}`
}
}

我们可以这样重构:

class PhoneNumber {
constructor(phoneNumber) {
this.phoneNumber = phoneNumber;
}
addAreaCode(areaCode) {
return `${areaCode}-${this.phoneNumber}`
}
}
class Person {
constructor(name, phoneNumber) {
this.name = name;
this.phoneNumber = new PhoneNumber(phoneNumber);
}
}

上面我们将Person类不太相关的方法addAreaCode 移动了自己该处理的类中。

通过这样做,两个类只做一件事,而不是让一个类做多件事。

总结

我们可以从复杂的类中提取代码,这些复杂的类可以将多种功能添加到自己的类中。

此外,我们可以将方法和字段移动到最常用的地方。

将值分配给参数值会造成混淆,因此我们应该在使用它们之前将其分配给变量。


代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://levelup.gitconnected....

收起阅读 »

我是如何在 Vue 项目中做代码分割的

通常为了开发效率,我们会使用 vue-cli 创建项目,这样创建的项目默认情况下编译是会对代码进行分割的。但是如果是自行配置的 webpack 环境的话,还是很有必要熟悉代码分割的相关知识的。为什么要做代码分割在配置 webpack 的过程...
继续阅读 »

通常为了开发效率,我们会使用 vue-cli 创建项目,这样创建的项目默认情况下编译是会对代码进行分割的。但是如果是自行配置的 webpack 环境的话,还是很有必要熟悉代码分割的相关知识的。

为什么要做代码分割

在配置 webpack 的过程中,很多时候我们的 webpack 入口只写了一个 entry: '${sourceDir}/index.js’,默认情况下只会生成一个 bundle 文件,包含了第三方库、公共代码及不同页面所用到的业务逻辑,这必然会造成该 bundle 文件体积过大,影响页面首次的加载速度,因此我们需要对代码进行分割,加快首次进入页面的速度。

代码分割思路

首先把第三方库、公共代码抽离出来,因为这些代码变动的频率小,可以打包成一个文件,这样每次上线文件都不发生变化,可以充分利用网络缓存加快文件下载速度,分割的细的话就是,第三方库为一个 js 文件, 公共代码为一个 js 文件。

然后,按照路由(页面)进行代码分割,每个页面生成一个 js 文件,这样每次首次进入就只加载公共代码和本页面用的的 js 文件, 而不用加载其它页面无关的代码。

最后,再进行精细分割的话,就是根据组件使用情况进行分割,来实现组件的懒加载,比如:页面中的不同 tab,可以根据 tab 的展示情况进行分割,把需要点击或者用户主动操作才能呈现的组件进行懒加载,这样就在页面级又进行了更细粒度的代码分割。

代码分割实战

第三方库及公共代码分割

第一步我们进行第三方库的分割,比如 vue、vue-router、vuex、axios 等三方库,把它们放到 vender.js 中,然后 utils、common 文件等放在 common.js 中。这些通过 webpack 的 entry 及 splitChunk 配置即可实现。

修改 entry 配置:

{
// ...
entry: {
// 把公共代码放到 common 里
common: [`${sourceDir}/utils/index.js`],
main: `${sourceDir}/index.js`,
},
// ...
}

splitChunk 配置:

{
optimization: {
// splitChunks 配置
splitChunks: {
cacheGroups: {
default: {
name: 'vendor',
// 把第三方库放到 vendor 里,包括 vue, vue-router, vuex 等
// 因为他们都是从 node_modules 里加载的,这里直接正则匹配
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
// 调整优先级,优先处理
priority: 10,
},
common: {
chunks: 'all',
name: 'common',
// 匹配 entry 里的 common 配置
test: 'common',
},
},
},
// runtime 代码放在 runtime 文件中
runtimeChunk: {
name: 'runtime',
},
}
}

另外就是 output 配置了,[name] 表示让 chunk 名称作为文件名, [chunkhash:8] 表示加上 hash,上线后不走缓存加载最新的代码。

{
output: {
path: path.join(__dirname, './dist'),
filename: 'static/[name].[chunkhash:8].bundle.js',
chunkFilename: 'static/[name].[chunkhash:8].bundle.js',
},
}

做完第三方库及公共代码分割,打包后生成的文件如下:

assets by path static/*.js 138 KiB
asset static/vendor.4391b08b.bundle.js 133 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: default)
asset static/main.0d6dab3a.bundle.js 3.9 KiB [emitted] [immutable] [minimized] (name: main)
asset static/runtime.bdaa3432.bundle.js 1.1 KiB [emitted] [immutable] [minimized] (name: runtime)
asset static/common.3f62940b.bundle.js 204 bytes [emitted] [immutable] [minimized] (name: common)
asset index.html 537 bytes [emitted]
asset static/main.acdc2841.bundle.css 127 bytes [emitted] [immutable] [minimized] (name: main)

我们可以看到代码分割到了不同的文件中,vender.js 包含了所有的第三方库,main.js 包含了我们各个页面的业务逻辑,公共代码在 common 中,runtime 包含了运行时代码,这样代码就分散到了不同的文件中,各司其职,且有利于同时进行加载。

但是 main.js 还是包含了多个页面的代码,如果只是进入首页的话,其它页面的代码就是多余的,接下来再进行优化。

按路由分割

这一个比较容易处理,只需改变下路由配置即可,以 () => import(path) 的方式加载页面组件:

const routes = [
{
path: '/',
// component: Home,
component: () => import('./pages/Home'),
},
{
path: '/todos',
// component: Todos,
component: () => import('./pages/Todos'),
},
{
path: '/about',
// component: About,
component: () => import('./pages/About'),
},
{
path: '/404',
// component: NotFound,
component: () => import('./pages/NotFound'),
},
{
path: '*',
redirect: '/404',
},
];

此时打包会看到多了很多文件,这是把不同页面的代码分割到了不同的 JS 文件中,只有访问对应的页面才会加载相关的代码。

assets by path static/*.js 142 KiB
asset static/vendor.4391b08b.bundle.js 133 KiB [emitted] [immutable] [minimized] (name: vendor) (id hint: default)
asset static/runtime.07c35c52.bundle.js 3.99 KiB [emitted] [immutable] [minimized] (name: runtime)
asset static/821.7ba5112d.bundle.js 1.89 KiB [emitted] [immutable] [minimized]
asset static/main.1697fd27.bundle.js 1.68 KiB [emitted] [immutable] [minimized] (name: main)
asset static/820.de28fd7b.bundle.js 562 bytes [emitted] [immutable] [minimized]
asset static/646.a902d0eb.bundle.js 406 bytes [emitted] [immutable] [minimized]
asset static/114.26876aa2.bundle.js 402 bytes [emitted] [immutable] [minimized]
asset static/common.3f62940b.bundle.js 204 bytes [emitted] [immutable] [minimized] (name: common)
assets by path static/*.css 127 bytes
asset static/main.beb1183a.bundle.css 75 bytes [emitted] [immutable] [minimized] (name: main)
asset static/821.cd9a22a5.bundle.css 52 bytes [emitted] [immutable] [minimized]
asset index.html 537 bytes [emitted]

当然,这个地方可能会有争议,争议的地方就是:「页面进入时就把所有页面的代码都下载下来,再进入其它页面不是更快吗?」。这就取决于项目情况了,看是着重于页面秒开,还是着重于页面切换体验。如果着重于秒开的话,配合 SSR 处理效果会更好。

更细粒度的分割

如果对于页面打开速度或性能有更高的要求,还可以做更细粒度的代码分割,比如页面中功能模块的懒加载。

这里以一个点击按钮时加载相应的组件为例,进行代码演示:

这里有一个 Load Lazy Demo 按钮,点击时才加载 LazyComponent 组件,LazyComponent 组件并没有什么特别之处,写法跟普通组件一样。

<template>
<button @click="loadLazyDemo">Load Lazy Demo</button>
<template v-if="showLazyComponent">
<lazy-component />
</template>
</template>

这里通过一个 showLazyComponent 控制组件的显示,当点击按钮时,把 showLazyComponent 置为 true,然后就加载 LazyComponent 对应的代码了。其实关键还是通过 () => import(path) 的方式引入组件。

<script>
export default {
data() {
return {
showLazyComponent: false,
};
},
methods: {
loadLazyDemo() {
this.showLazyComponent = true;
},
},
components: {
'lazy-component': () => import('../components/LazyComponent'),
},
};
</script>

K,以上就是我在 Vue 项目中做的代码分割的相关内容。

原文链接:https://segmentfault.com/a/1190000039859930

收起阅读 »

高质量代码的原则

简单性原则What:追求简单自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。Why:Bug 喜欢出现在复杂的地方软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代...
继续阅读 »

简单性原则

What:追求简单

自始至终都以最简单的逻辑编写代码,让编程初学者一眼就能看懂。在编程时我们要重视的是局部的完整性,而不是复杂的整体关联性。

Why:Bug 喜欢出现在复杂的地方

软件故障常集中在某一个区域,而这些区域都有一个共同的特点,那就是复杂。编写代码时如果追求简单易懂,代码就很难出现问题。不过,简单易懂的代码往往给人一种不够专业的感觉。这也是经验老到的程序员喜欢写老练高深的代码的原因。所以我们要有足够的定力来抵挡这种诱惑。

Do:编写自然的代码

放下高超的技巧,坚持用简单的逻辑编写代码。既然故障集中在代码复杂的区域,那我们只要让代码简单到让故障无处可藏即可。不要盲目地让代码复杂化、臃肿化,要保证代码简洁。

同构原则

What:力求规范

同等对待相同的东西,坚持不搞特殊。同等对待,举例来说就是同一个模块管理的数值全部采用同一单位、公有函数的参数个数统一等。

Why:不同的东西会更显眼

相同的东西用相同的形式表现能够使不同的东西更加突出。不同的东西往往容易产生 bug。遵循同构原则能让我们更容易嗅出代码的异样,从而找出问题所在。
统一的代码颇具美感,而美的东西一般更容易让人接受,因此统一的代码有较高的可读性。

Do:编写符合规范的代码

可靠与简单是代码不可或缺的性质,在编写代码时,务必克制住自己的表现欲,以规范为先。

对称原则

What:讲究形式上的对称

在思考一个处理时,也要想到与之成对的处理。比如有给标志位置 1 的处理,就要有给标志位置 0 的处理。

Why:帮助读代码的人推测后面的代码

具有对称性的代码能够帮助读代码的人推测后面的代码,提高其理解代码的速度。同时,对称性会给代码带来美感,这同样有助于他人理解代码。
此外,设计代码时将对称性纳入考虑的范围能防止我们在思考问题时出现遗漏。如果说代码的条件分支是故障的温床,那么对称性就是思考的框架,能有效阻止条件遗漏。

Do:编写有对称性的代码

在出现“条件”的时候,我们要注意它的“反条件”。每个控制条件都存在与之成对的反条件(与指示条件相反的条件)。要注意条件与反条件的统一,保证控制条件具有统一性。
我们还要考虑到例外情况并极力避免其发生。例外情况的特殊性会破坏对称性,成为故障的温床。特殊情况过多意味着需求没有得到整理。此时应重新审视需求,尽量从代码中剔除例外情况。
命名也要讲究对称性。命名时建议使用 set/getstart/stopbegin/ end 和 push/pop 等成对的词语。

层次原则

What:讲究层次

注意事物的主从关系、前后关系和本末关系等层次关系,整理事物的关联性。
不同层次各司其职,同种处理不跨越多个层次,这一点非常重要。比如执行了获取资源的处理,那么释放资源的处理就要在相同的层次进行。又比如互斥控制的标志位置 1 和置 0 的处理要在同一层次进行。

Why:层次结构有助于提高代码的可读性

有明确层次结构的代码能帮助读代码的人抽象理解代码的整体结构。读代码的人可以根据自身需要阅读下一层次的代码,掌握更加详细的信息。
这样可以提高代码的可读性,帮助程序员表达编码意图,降低 bug 发生的概率。

Do:编写有抽象层次结构的代码

在编写代码时设计各部分的抽象程度,构建层次结构。保证同一个层次中的所有代码抽象程度相同。另外,高层次的代码要通过外部视角描述低层次的代码。这样做能让调用低层次代码的高层次代码更加简单易懂。

线性原则

What:处理流程尽量走直线

一个功能如果可以通过多个功能的线性结合来实现,那它的结构就会非常简单。
反过来,用条件分支控制代码、毫无章法地增加状态数等行为会让代码变得难以理解。我们要避免做出这些行为,提高代码的可读性。

Why:直线处理可提高代码的可读性

复杂的处理流程是故障的温床。故障多出现在复杂的条件语句和循环语句中。另外,goto 等让流程出现跳跃的语句也是故障的多发地。
如果能让处理由高层次流向低层次,一气呵成,代码的可读性就会大幅提高。与此同时,可维护性也将提高,添加功能等改良工作将变得更加容易。
一般来说,自上而下的处理流程简单明快,易于理解。我们应避开复杂反复的处理流程。

Do:尽量不在代码中使用条件分支

尽量减少条件分支的数量,编写能让代码阅读者线性地看完整个处理流程的代码。
为此,我们需要把一些特殊的处理拿到主处理之外。保证处理的统一性,注意处理的流程。记得时不时俯瞰代码整体,检查代码是否存在过于复杂的部分。
另外,对于经过长期维护而变得过于复杂的部分,我们可以考虑对其进行重构。明确且可靠的设计不仅对我们自身有益,还可以给负责维护的人带来方便。

清晰原则

What:注意逻辑的清晰性

逻辑具有清晰性就代表逻辑能清楚证明自身的正确性。也就是说,我们编写的代码要让人一眼就能判断出没有问题。任何不明确的部分都要附有说明。

Why:消除不确定性

代码免不了被人一遍又一遍地阅读,所以代码必须保持较高的可读性。编写代码时如果追求高可读性,我们就不会采用取巧的方式编写代码,编写出的代码会非常自然。代码是给人看的,也是由人来修改的,所以我们必须以人为对象来编写代码。消除代码的不确定性是对自己的作品负责,这么做也可以为后续负责维护的人提供方便。

Do:编写逻辑清晰的代码

我们应选用直观易懂的逻辑。会给读代码的人带来疑问的部分要么消除,要么加以注释。另外,我们应使用任何人都能立刻理解且不存在歧义的术语。要特别注意变量名等一定不能没有意义。

安全原则

What:注意安全性

就是在编写代码时刻意将不可能的条件考虑进去。比如即便某个 if 语句一定成立,我们也要考虑 else 语句的情况;即便某个 case 语句一定成立,我们也要考虑 default 语句的情况;即便某个变量不可能为空,我们也要检查该变量是否为 null

Why:防止故障发展成重大事故

硬件提供的服务必须保证安全,软件也一样。硬件方面,比如取暖器,为防止倾倒起火,取暖器一般会配有倾倒自动断电装置。同样,设计软件时也需要考虑各种情况,保证软件在各种情况下都能安全地运行。这一做法在持续运营服务和防止数据损坏等方面有着积极的意义。

Do:编写安全的代码

选择相对安全的方法对具有不确定性的部分进行设计。列出所有可能的运行情况,确保软件在每种情况下都能安全运行。理解需求和功能,将各种情况正确分解到代码中,这样能有效提高软件安全运行的概率。
为此,我们也要将不可能的条件视为考察对象,对其进行设计和编程。不过,为了统一标准,我们在编写代码前最好规定哪些条件需要写,哪些条件不需要写。


原文链接:https://segmentfault.com/a/1190000039864589

收起阅读 »

TS实用工具类型

Partial<Type>构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。例子interface Todo { title: string; description: string; } fu...
继续阅读 »

Partial<Type>

构造类型Type,并将它所有的属性设置为可选的。它的返回类型表示输入类型的所有子类型。

例子

interface Todo {
title: string;
description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
title: 'organize desk',
description: 'clear clutter',
};

const todo2 = updateTodo(todo1, {
description: 'throw out trash',
});

Readonly<Type>

构造类型Type,并将它所有的属性设置为readonly,也就是说构造出的类型的属性不能被再次赋值。

例子

interface Todo {
title: string;
}

const todo: Readonly<Todo> = {
title: 'Delete inactive users',
};

todo.title = 'Hello'; // Error: cannot reassign a readonly property

这个工具可用来表示在运行时会失败的赋值表达式(比如,当尝试给冻结对象的属性再次赋值时)。

Object.freeze

function freeze<T>(obj: T): Readonly<T>;

Record<Keys, Type>

构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具可用来将某个类型的属性映射到另一个类型上。

例子

interface PageInfo {
title: string;
}

type Page = 'home' | 'about' | 'contact';

const x: Record<Page, PageInfo> = {
about: { title: 'about' },
contact: { title: 'contact' },
home: { title: 'home' },
};

Pick<Type, Keys>

从类型Type中挑选部分属性Keys来构造类型。

例子

interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Pick<Todo, 'title' | 'completed'>;

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
};

Omit<Type, Keys>

从类型Type中获取所有属性,然后从中剔除Keys属性后构造一个类型。

例子

interface Todo {
title: string;
description: string;
completed: boolean;
}

type TodoPreview = Omit<Todo, 'description'>;

const todo: TodoPreview = {
title: 'Clean room',
completed: false,
};

Exclude<Type, ExcludedUnion>

从类型Type中剔除所有可以赋值给ExcludedUnion的属性,然后构造一个类型。

例子

type T0 = Exclude<'a' | 'b' | 'c', 'a'>; // "b" | "c"
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Extract<Type, Union>

从类型Type中提取所有可以赋值给Union的类型,然后构造一个类型。

例子

type T0 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // "a"
type T1 = Extract<string | number | (() => void), Function>; // () => void

NonNullable<Type>

从类型Type中剔除nullundefined,然后构造一个类型。

例子

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

Parameters<Type>

由函数类型Type的参数类型来构建出一个元组类型。

例子

declare function f1(arg: { a: number; b: string }): void;

type T0 = Parameters<() => string>;
// []
type T1 = Parameters<(s: string) => void>;
// [s: string]
type T2 = Parameters<<T>(arg: T) => T>;
// [arg: unknown]
type T3 = Parameters<typeof f1>;
// [arg: { a: number; b: string; }]
type T4 = Parameters<any>;
// unknown[]
type T5 = Parameters<never>;
// never
type T6 = Parameters<string>;
// never
// Type 'string' does not satisfy the constraint '(...args: any) => any'.
type T7 = Parameters<Function>;
// never
// Type 'Function' does not satisfy the constraint '(...args: any) => any'.

ConstructorParameters<Type>

由构造函数类型来构建出一个元组类型或数组类型。
由构造函数类型Type的参数类型来构建出一个元组类型。(若Type不是构造函数类型,则返回never)。

例子

type T0 = ConstructorParameters<ErrorConstructor>;
// [message?: string | undefined]
type T1 = ConstructorParameters<FunctionConstructor>;
// string[]
type T2 = ConstructorParameters<RegExpConstructor>;
// [pattern: string | RegExp, flags?: string | undefined]
type T3 = ConstructorParameters<any>;
// unknown[]

type T4 = ConstructorParameters<Function>;
// never
// Type 'Function' does not satisfy the constraint 'new (...args: any) => any'.

ReturnType<Type>

由函数类型Type的返回值类型构建一个新类型。

例子

type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<(<T>() => T)>; // {}
type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[]
type T4 = ReturnType<typeof f1>; // { a: number, b: string }
type T5 = ReturnType<any>; // any
type T6 = ReturnType<never>; // any
type T7 = ReturnType<string>; // Error
type T8 = ReturnType<Function>; // Error

InstanceType<Type>

由构造函数类型Type的实例类型来构建一个新类型。

例子

class C {
x = 0;
y = 0;
}

type T0 = InstanceType<typeof C>; // C
type T1 = InstanceType<any>; // any
type T2 = InstanceType<never>; // any
type T3 = InstanceType<string>; // Error
type T4 = InstanceType<Function>; // Error

Required<Type>

构建一个类型,使类型Type的所有属性为required
与此相反的是Partial

例子

interface Props {
a?: number;
b?: string;
}

const obj: Props = { a: 5 }; // OK

const obj2: Required<Props> = { a: 5 }; // Error: property 'b' missing

ThisParameterType<Type>

从函数类型中提取 this 参数的类型。
若函数类型不包含 this 参数,则返回 unknown 类型。

例子

function toHex(this: Number) {
return this.toString(16);
}

function numberToString(n: ThisParameterType<typeof toHex>) {
return toHex.apply(n);
}

OmitThisParameter<Type>

Type类型中剔除 this 参数。
若未声明 this 参数,则结果类型为 Type 。
否则,由Type类型来构建一个不带this参数的类型。
泛型会被忽略,并且只有最后的重载签名会被采用。

例子

function toHex(this: Number) {
return this.toString(16);
}

const fiveToHex: OmitThisParameter<typeof toHex> = toHex.bind(5);

console.log(fiveToHex());

ThisType<Type>

这个工具不会返回一个转换后的类型。
它作为上下文的this类型的一个标记。
注意,若想使用此类型,必须启用--noImplicitThis

例子

// Compile with --noImplicitThis

type ObjectDescriptor<D, M> = {
data?: D;
methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}

let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx; // Strongly typed this
this.y += dy; // Strongly typed this
},
},
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

上面例子中,makeObject参数里的methods对象具有一个上下文类型ThisType<D & M>,因此methods对象的方法里this的类型为{ x: number, y: number } & { moveBy(dx: number, dy: number): number }

lib.d.ts里,ThisType<T>标识接口是个简单的空接口声明。除了在被识别为对象字面量的上下文类型之外,这个接口与一般的空接口没有什么不同。

原文链接:https://segmentfault.com/a/1190000039868550

收起阅读 »

复杂场景下的h5与小程序通信

复杂场景下的h5与小程序通信一、背景在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。但在套壳小程序中,h5与...
继续阅读 »

复杂场景下的h5与小程序通信

一、背景

在套壳小程序盛行的当下, h5调用小程序能力来打破业务边界已成为家常便饭,h5与小程序的结合,极大地拓展了h5的能力边界,丰富了h5的功能。使许多以往纯h5只能想想或者实现难度极大的功能变得轻松简单。
但在套壳小程序中,h5与小程序通信存在以下几个问题:
  • 注入小程序全局变量的时机不确定,可能调用的时候不存在小程序变量。和全局变量my相关的判断满天飞,每个使用的地方都需要判断是否已注入变量,否则就要创建监听。
  • 小程序处理后的返回结果可能有多种,h5需要在具体使用时监听多个结果进行处理。
  • 一旦监听建立,就无法取消,在组件销毁时如果没有判断组件状态容易导致内存泄漏。

二、在业务内的实践

  • 因业务的特殊性,需要投放多端,小程序sdk的加载没有放到head里面,而是在应用启动时动态判断是小程序环境时自动注入的方式:

export function injectMiniAppScript() {
if (isAlipayMiniApp() || isAlipayMiniAppWebIDE()) {
const s = document.createElement('script');

s.src = 'https://appx/web-view.min.js';
s.onload = () => {
// 加载完成时触发自定义事件
const customEvent = new CustomEvent('myLoad', { detail:'' });
document.dispatchEvent(customEvent);
};

s.onerror = (e) => {
// 加载失败时上传日志
uploadLog({
tip: `INJECT_MINIAPP_SCRIPT_ERROR`,
});
};

document.body.insertBefore(s, document.body.firstChild);
}
}

加载脚本完成后,我们就可以调用my.postMessagemy.onMessage进行通信(统一约定h5发送消息给小程序时,必须带action,小程序根据action处理业务逻辑,同时小程序处理完成的结果必须带type,h5在不同的业务场景下通过my.onMessage处理不同type的响应),比如典型的,h5调用小程序签到:
h5部分代码如下:

// 处理扫脸签到逻辑
const faceVerify = (): Promise => {

return new Promise((resolve) => {
const handle = () => {
window.my.onMessage = (result: AlipaySignResult) => {
if (result.type === 'FACE_VERIFY_TIMEOUT' ||
result.type === 'DO_SIGN' ||
result.type === 'FACE_VERIFY' ||
result.type === 'LOCATION' ||
result.type === 'LOCATION_UNBELIEVABLE' ||
result.type === 'NOT_IN_ALIPAY') {
resolve(result);
}
};

window.my.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
};

if (window.my) {
handle();
} else {
// 先记录错误日志
sendErrors('/threehours.3hours-errors.NO_MY_VARIABLE', { msg: '变量不存在' });
// 监听load事件
document.addEventListener('myLoad', handle);
}
});
};

实际上还是相当繁琐的,使用时都要先判断my是否存在,进行不同的处理,一两处还好,多了就受不了了,而且这种散乱的代码遍布各处,甚至是不同的应用,于是,我封装了下面这个sdkminiAppBus,先来看看怎么用,还是上面的场景

// 处理扫脸签到逻辑
const faceVerify = (): Promise => {
miniAppBus.postMessage({ action: SIGN_CONSTANT.FACE_VERIFY, activityId: id, userId: user.userId });
return miniAppBus.subscribeAsync([
'FACE_VERIFY_TIMEOUT',
'DO_SIGN',
'FACE_VERIFY',
'LOCATION',
'LOCATION_UNBELIEVABLE',
'NOT_IN_ALIPAY',
])
};
  • 可以看到,无论是postMessage还是监听message,都不需要再关注环境,直接使用即可。在业务场景复杂的情况下,提效尤为明显。

三、实现及背后的思考

  • 为了满足不同场景和使用的方便,公开暴露的interface如下:

interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}

subscribe:函数接收两个参数,
type:需要订阅的type,可以是字符串,也可以是数组。
callback:回调函数。
subscribeAsync:接收type(同上),返回Promise对象,值得注意的是,目前只要监听到其中一个type返回,promise就resolved,未来对同一个action对应多个结果type时存在问题,需要拓展,不过目前还未遇到此类场景。
unsubscribe:取消订阅。
postMessage:postMessage替代,无需关注环境变量。

完整代码:

import { injectMiniAppScript } from './tools';

/**
* @description 小程序返回结果
* @export
* @interface MiniAppMessage
*/

interface MiniAppMessageBase {
type: string;
}

type MiniAppMessage = MiniAppMessageBase & {
[P in keyof T]: T[P]
}
/**
* @description 小程序接收消息
* @export
* @interface MessageToMiniApp
*/
export interface MessageToMiniApp {
action: string;
[x: string]: unknown
}

interface MiniAppMessageSubscriber {
(params: MiniAppMessage): void
}
interface MiniAppEventBus {
/**
* @description 回调函数订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @param {MiniAppMessageSubscriber} callback
* @memberof MiniAppEventBus
*/
subscribe(type: string | string[], callback: MiniAppMessageSubscriber): void;
/**
* @description Promise 订阅单个、或多个type
* @template T
* @param {(string | string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
subscribeAsync(type: string | string[]): Promise>;
/**
* @description 取消订阅单个、或多个type
* @param {(string | string[])} type
* @returns {Promise}
* @memberof MiniAppEventBus
*/
unSubscribe(type: string | string[]): Promise;
/**
* @description postMessage替代,无需关注环境变量
* @param {MessageToMiniApp} msg
* @returns {Promise}
* @memberof MiniAppEventBus
*/
postMessage(msg: MessageToMiniApp): Promise;
}
class MiniAppEventBus implements MiniAppEventBus{

/**
* @description: 监听函数
* @type {Map}
* @memberof MiniAppEventBus
*/
listeners: Map;
constructor() {
this.listeners = new Map>>();
this.init();
}

/**
* @description 初始化
* @private
* @memberof MiniAppEventBus
*/
private init() {
if (!window.my) {
// 引入脚本
injectMiniAppScript();
}

this.startListen();
}

/**
* @description 保证my变量存在的时候执行函数func
* @private
* @param {Function} func
* @returns
* @memberof MiniAppEventBus
*/
private async ensureEnv(func: Function) {
return new Promise((resolve) => {
const promiseResolve = () => {
resolve(func.call(this));
};

// 全局变量
if (window.my) {
promiseResolve();
}

document.addEventListener('myLoad', promiseResolve);
});
}

/**
* @description 监听小程序消息
* @private
* @memberof MiniAppEventBus
*/
private listen() {
window.my.onMessage = (msg: MiniAppMessage) => {
this.dispatch(msg.type, msg);
};
}

private async startListen() {
return this.ensureEnv(this.listen);
}

/**
* @description 发送消息,必须包含action
* @param {MessageToMiniApp} msg
* @returns
* @memberof MiniAppEventBus
*/
public postMessage(msg: MessageToMiniApp) {
return new Promise((resolve) => {
const realPost = () => {
resolve(window.my.postMessage(msg));
};

resolve(this.ensureEnv(realPost));
});
}

/**
* @description 订阅消息,支持单个或多个
* @template T
* @param {(string|string[])} type
* @param {MiniAppMessageSubscriber} callback
* @returns
* @memberof MiniAppEventBus
*/
public subscribe(type: string | string[], callback: MiniAppMessageSubscriber) {
const subscribeSingleAction = (type: string, cb: MiniAppMessageSubscriber) => {
let listeners = this.listeners.get(type) || [];

listeners.push(cb);
this.listeners.set(type, listeners);
};

this.forEach(type,(type:string)=>subscribeSingleAction(type,callback));
}

private forEach(type:string | string[],cb:(type:string)=>void){
if (typeof type === 'string') {
return cb(type);
}

for (const key in type) {
if (Object.prototype.hasOwnProperty.call(type, key)) {
const element = type[key];

cb(element);
}
}
}

/**
* @description 异步订阅
* @template T
* @param {(string|string[])} type
* @returns {Promise>}
* @memberof MiniAppEventBus
*/
public async subscribeAsync(type: string | string[]): Promise> {
return new Promise((resolve, _reject) => {
this.subscribe(type, resolve);
});
}

/**
* @description 触发事件
* @param {string} type
* @param {MiniAppMessage} msg
* @memberof MiniAppEventBus
*/
public async dispatch(type: string, msg: MiniAppMessage) {
let listeners = this.listeners.get(type) || [];

listeners.map(i => {
if (typeof i === 'function') {
i(msg);
}
});
}

public async unSubscribe(type:string | string[]){
const unsubscribeSingle = (type: string) => {
this.listeners.set(type, []);
};

this.forEach(type,(type:string)=>unsubscribeSingle(type));
}
}

export default new MiniAppEventBus();
  • class内部处理了脚本加载,变量判断,消息订阅一系列逻辑,使用时不再关注。

四、小程序内部的处理

  • 定义action handle,通过策略模式解耦:

const actionHandles = {
async FACE_VERIFY(){},
async GET_STEP(){},
async UPLOAD_HASH(){},
async GET_AUTH_CODE(){},
...// 其他action
}
....
// 在webview的消息监听函数中
async startProcess(e) {
const data = e.detail;
// 根据不同的action调用不同的handle处理
const handle = actionHandles[data.action];
if (handle) {

return actionHandles[data.action](this, data)
}
return uploadLogsExtend({
tip: STRING_CONTANT.UNKNOWN_ACTIONS,
data
})
}
  • 使用起来也是得心顺畅,舒服。

其他

类型完备,使用时智能提示,方便快捷。

原文链接:https://segmentfault.com/a/1190000023360940

收起阅读 »

小程序自动化测试

背景近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小...
继续阅读 »

背景

近期团队打算做一个小程序自动化测试的工具,期望能够做到业务人员操作一遍小程序后,自动还原之前的操作路径,并且捕获操作过程中发生的异常,以此来判断这次发布是否会影响小程序的基础功能。


上述描述看似简单,但是中间还是有些难点的,第一个难点就是如何在业务人员操作小程序的时候记录操作路径,第二个难点就是如何将记录的操作路径进行还原。

自动化 SDK

如何将操作路径还原这个问题,首选官方提供的 SDK: miniprogram-automator

小程序自动化 SDK 为开发者提供了一套通过外部脚本操控小程序的方案,从而实现小程序自动化测试的目的。通过该 SDK,你可以做到以下事情:

  • 控制小程序跳转到指定页面
  • 获取小程序页面数据
  • 获取小程序页面元素状态
  • 触发小程序元素绑定事件
  • 往 AppService 注入代码片段
  • 调用 wx 对象上任意接口
  • ...

上面的描述都来自官方文档,建议阅读后面内容之前可以先看看官方文档,当然如果之前用过 puppeteer ,也可以快速上手,api 基本一致。下面简单介绍下 SDK 的使用方式。

// 引入sdk
const automator = require('miniprogram-automator')

// 启动微信开发者工具
automator.launch({
// 微信开发者工具安装路径下的 cli 工具
// Windows下为安装路径下的 cli.bat
// MacOS下为安装路径下的 cli
cliPath: 'path/to/cli',
// 项目地址,即要运行的小程序的路径
projectPath: 'path/to/project',
}).then(async miniProgram => { // miniProgram 为 IDE 启动后的实例
// 启动小程序里的 index 页面
const page = await miniProgram.reLaunch('/page/index/index')
// 等待 500 ms
await page.waitFor(500)
// 获取页面元素
const element = await page.$('.main-btn')
// 点击元素
await element.tap()
// 关闭 IDE
await miniProgram.close()
})

有个地方需要提醒一下:使用 SDK 之前需要开启开发者工具的服务端口,要不然会启动失败。




捕获用户行为

有了还原操作路径的办法,接下来就要解决记录操作路径的难题了。

在小程序中,并不能像 web 中通过事件冒泡的方式在 window 中捕获所有的事件,好在小程序所以的页面和组件都必须通过 Page 、Component 方法来包装,所以我们可以改写这两个方法,拦截传入的方法,并判断第一个参数是否为 event 对象,以此来捕获所有的事件。

// 暂存原生方法
const originPage = Page
const originComponent = Component

// 改写 Page
Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
originPage(params)
}
// 改写 Component
Component = (params) => {
if (params.methods) {
const { methods } = params
const names = Object.keys(methods)
for (const name of names) {
// 进行方法拦截
if (typeof methods[name] === 'function') {
methods[name] = hookMethod(name, methods[name], true)
}
}
}
originComponent(params)
}

const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (evt && evt.target && evt.type) {
// 记录用户行为
}
return method.apply(this, args)
}
}

这里的代码只是代理了所有的事件方法,并不能用来还原用户的行为,要还原用户行为还必须知道该事件类型是否是需要的,比如点击、长按、输入。

const evtTypes = [
'tap', // 点击
'input', // 输入
'confirm', // 回车
'longpress' // 长按
]
const hookMethod = (name, method) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
// 记录用户行为
}
return method.apply(this, args)
}
}

确定事件类型之后,还需要明确点击的元素到底是哪个,但是小程序里面比较坑的地方就是,event 对象的 target 属性中,并没有元素的类名,但是可以获取元素的 dataset。


为了准确的获取元素,我们需要在构建中增加一个步骤,修改 wxml 文件,将所有元素的 class 属性复制一份到 

<!-- 构建前 -->
<view class="close-btn"></view>
<view class="{{mainClassName}}"></view>
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn"></view>
<view class="{{mainClassName}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"{{mainClassName}}"></view>

但是获取到 class 之后,又会有另一个坑,小程序的自动化测试工具并不能直接获取页面里自定义组件中的元素,必须先获取自定义组件。

<!-- Page -->
<toast text="loading" show="{{showToast}}" />
<!-- Component -->
<view class="toast" wx:if="{{show}}">
<text class="toast-text">{{text}}</text>
<view class="toast-close" />
</view>
// 如果直接查找 .toast-close 会得到 null
const element = await page.$('.toast-close')
element.tap() // Error!

// 必须先通过自定义组件的 tagName 找到自定义组件
// 再从自定义组件中通过 className 查找对应元素
const element = await page.$('toast .toast-close')
element.tap()

所以我们在构建操作的时候,还需要为元素插入 tagName。

<!-- 构建前 -->
<view class="close-btn" />
<toast text="loading" show="{{showToast}}" />
<!-- 构建后 -->
<view class="close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"close-btn" style="box-sizing: border-box; color: rgb(221, 17, 68);">"view" />
<toast text="loading" show="{{showToast}}" style="box-sizing: border-box; color: rgb(221, 17, 68);">"toast" />

现在我们可以继续愉快的记录用户行为了。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
actions.push({
time: Date.now(),
type,
query,
value
})
}

// 代理事件方法
const hookMethod = (name, method, isComponent) => {
return function(...args) {
const [evt] = args // 取出第一个参数
// 判断是否为 event 对象
if (
evt && evt.target && evt.type &&
evtTypes.includes(evt.type) // 判断事件类型
) {
const { type, target, detail } = evt
const { id, dataset = {} } = target
const { className = '' } = dataset
const { value = '' } = detail // input事件触发时,输入框的值
// 记录用户行为
let query = ''
if (isComponent) {
// 如果是组件内的方法,需要获取当前组件的 tagName
query = `${this.dataset.tagName} `
}
if (id) {
// id 存在,则直接通过 id 查找元素
query += id
} else {
// id 不存在,才通过 className 查找元素
query += className
}
addAction(type, query, value)
}
return method.apply(this, args)
}
}

到这里已经记录了用户所有的点击、输入、回车相关的操作。但是还有滚动屏幕的操作没有记录,我们可以直接代理 Page 的 onPageScroll 方法。

// 记录用户行为的数组
const actions = [];
// 添加用户行为
const addAction = (type, query, value = '') => {
if (type === 'scroll' || type === 'input') {
// 如果上一次行为也是滚动或输入,则重置 value 即可
const last = this.actions[this.actions.length - 1]
if (last && last.type === type) {
last.value = value
last.time = Date.now()
return
}
}
actions.push({
time: Date.now(),
type,
query,
value
})
}

Page = (params) => {
const names = Object.keys(params)
for (const name of names) {
// 进行方法拦截
if (typeof obj[name] === 'function') {
params[name] = hookMethod(name, params[name], false)
}
}
const { onPageScroll } = params
// 拦截滚动事件
params.onPageScroll = function (...args) {
const [evt] = args
const { scrollTop } = evt
addAction('scroll', '', scrollTop)
onPageScroll.apply(this, args)
}
originPage(params)
}

这里有个优化点,就是滚动操作记录的时候,可以判断一下上次操作是否也为滚动操作,如果是同一个操作,则只需要修改一下滚动距离即可,因为两次滚动可以一步到位。同理,输入事件也是,输入的值也可以一步到位。

还原用户行为

用户操作完毕后,可以在控制台输出用户行为的 json 文本,把 json 文本复制出来后,就可以通过自动化工具运行了。

// 引入sdk
const automator = require('miniprogram-automator')

// 用户操作行为
const actions = [
{ type: 'tap', query: 'goods .title', value: '', time: 1596965650000 },
{ type: 'scroll', query: '', value: 560, time: 1596965710680 },
{ type: 'tap', query: 'gotoTop', value: '', time: 1596965770000 }
]

// 启动微信开发者工具
automator.launch({
projectPath: 'path/to/project',
}).then(async miniProgram => {
let page = await miniProgram.reLaunch('/page/index/index')

let prevTime
for (const action of actions) {
const { type, query, value, time } = action
if (prevTime) {
// 计算两次操作之间的等待时间
await page.waitFor(time - prevTime)
}
// 重置上次操作时间
prevTime = time

// 获取当前页面实例
page = await miniProgram.currentPage()
switch (type) {
case 'tap':
const element = await page.$(query)
await element.tap()
break;
case 'input':
const element = await page.$(query)
await element.input(value)
break;
case 'confirm':
const element = await page.$(query)
await element.trigger('confirm', { value });
break;
case 'scroll':
await miniProgram.pageScrollTo(value)
break;
}
// 每次操作结束后,等待 5s,防止页面跳转过程中,后面的操作找不到页面
await page.waitFor(5000)
}

// 关闭 IDE
await miniProgram.close()
})

这里只是简单的还原了用户的操作行为,实际运行过程中,还会涉及到网络请求和 localstorage 的 mock,这里不再展开讲述。同时,我们还可以接入 jest 工具,更加方便用例的编写。

总结

看似很难的需求,只要用心去发掘,总能找到对应的解决办法。另外微信小程序的自动化工具真的有很多坑,遇到问题可以先到小程序社区去找找,大部分坑都有前人踩过,还有一些一时无法解决的问题只能想其他办法来规避。最后祝愿天下无 bug。

原文链接:https://segmentfault.com/a/1190000023555693


收起阅读 »

键盘设置如何优化小程序使用体验?

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。Input 组件的 type 属性从小程序的 1.0 版本开始,...
继续阅读 »

在小程序开发过程中,用户输入是必不可少的,我们经常会需要用户输入一些内容,来完成产品收集用户信息的需求。

在这种情况下,我们可以考虑借助小程序提供的一些和键盘相关的 API 来优化小程序的使用体验。

Input 组件的 type 属性


从小程序的 1.0 版本开始,就支持为 input 组件设置 type,不同的 type 会显示不同的手机键盘。默认情况下,显示的是 text 文本输入键盘,这个键盘的特点是显示所有的内容,可以适用于所有的场景。

但,适用于所有场景也就意味着不适用于所有场景,总会在每一个场景中有着种种不便,因此,在实际的开发中,为了获得更佳的体验,你可以通过设置不同的 Type 来控制实际的键盘显示情况。


除了默认的 text 类以外,你还可以使用 number(数字输入键盘)、idcard 身份证输入键盘和 digit 带小数点的数字键盘。


你可以根据自己的实际使用场景来设置不同的类型,比如说

  • 如果你的小程序的验证码都是数字的,那么你给出一个 text 类型的键盘,显然不如给一个 number 类型的键盘更合适。
  • 如果你的小程序中涉及到了手机号的输入,那么这种情况下你就可以选择使用 number 类型的键盘,来优化用户输入时的体验。

这里的思路是类似的,当你预期用户输入的内容只有数字,就可以考虑 numberdigitidcard 等类型,来优化你的小程序的实际使用体验。


## 总结

input 组件默认提供的 四种 type ,可以通过选择不同的类型,从而获得不同的体验效果,从而对于你的小程序体验进行优化和推进。

原文链接:https://segmentfault.com/a/1190000025160488

收起阅读 »

小程序canvas实现图片压缩

我们需要在选择图片后对图片做一次安全校验启用云开发现在我们需要一个 后端接口 来实现图片的 安全校验 功能这时候临时搭个Node服务好像不太现实又不是什么正经项目于是就想到了微信的云开发功能用起来真实方便快捷至于图片的校...
继续阅读 »




我们需要在选择图片后

对图片做一次安全校验

启用云开发

现在我们需要一个 后端接口 来实现图片的 安全校验 功能

这时候临时搭个Node服务好像不太现实

又不是什么正经项目

于是就想到了微信的云开发功能

用起来真实方便快捷

至于图片的校验方法

直接用云函数调用 security.imgSecCheck 接口就好了

流程

chooseImage() {
/// 用户选择图片
wx.chooseImage({
count: 1,
sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'],
success: async res => {
if (res.errMsg === 'chooseImage:ok') {
wx.showLoading({ title: '图片加载中' })
// 获取图片临时地址
const path = res.tempFilePaths[0]
// 将图片地址实例化为图片
const image = await loadImage(path, this.canvas)
// 压缩图片
const filePath = await compress.call(this, image, 'canvas_compress')
// 校验图片合法性
const imgValid = await checkImage(filePath)
wx.hideLoading()
if (!imgValid) return
// 图片安全检测通过,执行后续操作
...
}
})
}


所以在图片上传前要先对超出尺寸的图片进行压缩处理
基本逻辑就是

超出尺寸的图片等比例缩小就好了

我们先要有一个canvas元素

用来处理需要压缩的图片

<template>
<view class="menu-background">
<view class="item replace" bindtap="chooseImage">
<i class="iconfont icon-image"></i>
<text class="title">图片</text>
<text class="sub-title">图片仅供本地使用</text>
</view>
//
// canvas
//
<canvas
type="2d"
id="canvas_compress"
class="canvas-compress"
style="width:
{{canvasCompress.width}}px; height: {{canvasCompress.height}}px"
/>

</view>
</template>

将canvas移到视野不可见到位置

.canvas-compress
position absolute
left 0
top 1000px

图片进行压缩处理

/**
* 压缩图片
* 将尺寸超过规范的图片最小限度压缩
* @param {Image} image 需要压缩的图片实例
* @param {String} canvasId 用来处理压缩图片的canvas对应的canvasId
* @param {Object} config 压缩的图片规范 -> { maxWidth 最大宽度, maxHeight 最小宽度 }
* @return {Promise} promise返回 压缩后的 图片路径
*/
export default function (image, canvasId, config = { maxWidth: 750, maxHeight: 1334 }) {
// 引用的组件传入的this作用域
const _this = this
return new Promise((resolve, reject) => {
// 获取图片原始宽高
let width = image.width
let height = image.height
// 宽度 > 最大限宽 -> 重置尺寸
if (width > config.maxWidth) {
const ratio = width / config.maxWidth
width = config.maxWidth
height = height / ratio
}
// 高度 > 最大限高度 -> 重置尺寸
if (height > config.maxHeight) {
const ratio = height / config.maxHeight
height = config.maxHeight
width = width / ratio
}
// 设置canvas的css宽高
_this.canvasCompress.width = width
_this.canvasCompress.height = height
const query = this.createSelectorQuery()
query
.select(`#${canvasId}`)
.fields({ node: true, size: true })
.exec(async res => {
// 获取 canvas 实例
const canvas = res[0].node
// 获取 canvas 绘图上下文
const ctx = canvas.getContext('2d')
// 根据设备dpr处理尺寸
const dpr = wx.getSystemInfoSync().pixelRatio
canvas.width = width * dpr
canvas.height = height * dpr
ctx.scale(dpr, dpr)
// 将图片绘制到 canvas
ctx.drawImage(image, 0, 0, width, height)
// 将canvas图片上传到微信临时文件
wx.canvasToTempFilePath({
canvas,
x: 0,
y: 0,
destWidth: width,
destHeight: height,
complete (res) {
if (res.errMsg === 'canvasToTempFilePath:ok') {
// 返回临时文件路径
resolve(res.tempFilePath)
}
},
fail(err) {
reject(err)
}
})
})
})
}

图片安全校验

云函数 checkImage.js

const cloud = require('wx-server-sdk')
cloud.init({
env: cloud.DYNAMIC_CURRENT_ENV
})
/**
* 校验图片合法性
* @param {*} event.fileID 微信云存储的图片ID
* @return {Number} 0:校验失败;1:校验通过
*/
exports.main = async (event, context) => {
const contentType = 'image/png'
const fileID = event.fileID
try {
// 根据fileID下载图片
const file = await cloud.downloadFile({
fileID
})
const value = file.fileContent
// 调用 imgSecCheck 借口,校验不通过接口会抛错
// 必要参数 media { contentType, value }
const result = await cloud.openapi.security.imgSecCheck({
media: {
contentType,
value
}
})
return 1
} catch (err) {
return 0
}
}

组件调用云函数封装

/**
* 校验图片是否存在敏感信息
* @param { String } filePath
* @return { Promise } promise返回校验结果
*/
export default function (filePath) {
return new Promise((resolve, reject) => {
// 先将图片上传到云开发存储
wx.cloud.uploadFile({
cloudPath: `${new Date().getTime()}.png`,
filePath,
success (res) {
// 调用云函数-checkImage
wx.cloud.callFunction({
name: 'checkImage',
data: {
fileID: res.fileID
},
success (res) {
// res.result -> 0:存在敏感信息;1:校验通过
resolve(res.result)
if (!res.result) {
wx.showToast({
title: '图片可能含有敏感信息, 请重新选择',
icon: 'none'
})
}
},
fail (err) {
reject(err)
}
})
},
fail (err) {
reject(err)
}
})
})
}

原文链接:https://segmentfault.com/a/1190000038685508


收起阅读 »

小程序的「获取URL Scheme」能力

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。什么是 URL Scheme微信提供了一个接口,可以生成如 weixin://dl/business/?t=...
继续阅读 »

最近,微信小程序更新了一项新的能力:「获取URL Scheme」,这是一项非常有用的功能,你可以借助他,在微信生态中实现各种有意思的营销方式。

什么是 URL Scheme

微信提供了一个接口,可以生成如 weixin://dl/business/?t= *TICKET* 的 URL Scheme。你可以在系统自带的浏览器,比如 Safari 中访问这个地址,自动跳转到你自己的微信小程序中。

URL Scheme 能实现什么?

URL Scheme 的用途最大自然是各种营销用途,比如短信营销。不过,如果我们发散思维,就可以知道,URL Scheme 可以有更多的用途。

URL Scheme 在 iOS 系统应用中是比较多的,不少 iOS 的 Power User 都会借助 URL Scheme 来自定义自己的手机中的一些操作,实现特别的操作。我们可以参考 iOS 的 Power User 的用法,理解微信的 URL Scheme 的用法

  • 通过快捷指令来打开特定的 App
  • 在浏览器中嵌入 URL Scheme 来打开应用的特定页面。

如果我们将这些能力迁移到微信生态中,就可以发现,这里我们同样可以实现:

  • 在公众号网页中嵌入 URL Scheme ,从而实现公众号内网页与小程序无缝链接
  • 在短信中嵌入 URL Scheme ,从而实现短信营销,轻松的与自己的产品整合
  • 根据 URL Scheme ,生成一些特殊的二维码,嵌入在图片中

不仅如此,因为目前微信的安装率远高于普通 App,因此,你在进行营销的时候,就再也无需担心用户没有安装自己的 App,大可以先让用户进入到小程序,成为用户后,再引导用户下载 App,提升产品体验

URL Scheme 的劣势

虽然很好,不过 URL Scheme 目前还有一些问题,比如只限于国内非个人主体小程序,对于个人开发者来说就无法使用了。

总结

URL Scheme 的开放,对于微信生态来说,是一个很有力的工具,开发者可以借助与 URL Scheme 来完成自己在微信生态中的推广。在未来,我们可以看到,越来越多的开发者借助于 URL Scheme ,来实现一些很有意思的营销方式。

让我们拭目以待。

原文链接:https://segmentfault.com/a/1190000038919562


收起阅读 »

Web 安全 之 Clickjacking

Clickjacking ( UI redressing )在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。什么是点击劫持点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了...
继续阅读 »

Clickjacking ( UI redressing )

在本节中,我们将解释什么是 clickjacking 点击劫持,并描述常见的点击劫持攻击示例,以及讨论如何防御这些攻击。

什么是点击劫持

点击劫持是一种基于界面的攻击,通过诱导用户点击钓鱼网站中的被隐藏了的可操作的危险内容。

例如:某个用户被诱导访问了一个钓鱼网站(可能是点击了电子邮件中的链接),然后点击了一个赢取大奖的按钮。实际情况则是,攻击者在这个赢取大奖的按钮下面隐藏了另一个网站上向其他账户进行支付的按钮,而结果就是用户被诱骗进行了支付。这就是一个点击劫持攻击的例子。这项技术实际上就是通过 iframe 合并两个页面,真实操作的页面被隐藏,而诱骗用户点击的页面则显示出来。点击劫持攻击与 CSRF 攻击的不同之处在于,点击劫持需要用户执行某种操作,比如点击按钮,而 CSRF 则是在用户不知情或者没有输入的情况下伪造整个请求。


针对 CSRF 攻击的防御措施通常是使用 CSRF token(针对特定会话、一次性使用的随机数)。而点击劫持无法则通过 CSRF token 缓解攻击,因为目标会话是在真实网站加载的内容中建立的,并且所有请求均在域内发生。CSRF token 也会被放入请求中,并作为正常行为的一部分传递给服务器,与普通会话相比,差异就在于该过程发生在隐藏的 iframe 中。

如何构造一个基本的点击劫持攻击

点击劫持攻击使用 CSS 创建和操作图层。攻击者将目标网站通过 iframe 嵌入并隐藏。使用样式标签和参数的示例如下:

<head>
<style>
#target_website {
position:relative;
width:128px;
height:128px;
opacity:0.00001;
z-index:2;
}
#decoy_website {
position:absolute;
width:300px;
height:400px;
z-index:1;
}
</style>
</head>
...
<body>
<div id="decoy_website">
...decoy web content here...
</div>
<iframe id="target_website" src="https://vulnerable-website.com">
</iframe>
</body>

目标网站 iframe 被定位在浏览器中,使用适当的宽度和高度位置值将目标动作与诱饵网站精确重叠。无论屏幕大小,浏览器类型和平台如何,绝对位置值和相对位置值均用于确保目标网站准确地与诱饵重叠。z-index 决定了 iframe 和网站图层的堆叠顺序。透明度被设置为零,因此 iframe 内容对用户是透明的。浏览器可能会基于 iframe 透明度进行阈值判断从而自动进行点击劫持保护(例如,Chrome 76 包含此行为,但 Firefox 没有),但攻击者仍然可以选择适当的透明度值,以便在不触发此保护行为的情况下获得所需的效果。

预填写输入表单

一些需要表单填写和提交的网站允许在提交之前使用 GET 参数预先填充表单输入。由于 GET 参数在 URL 中,那么攻击者可以直接修改目标 URL 的值,并将透明的“提交”按钮覆盖在诱饵网站上。

Frame 拦截脚本

只要网站可以被 frame ,那么点击劫持就有可能发生。因此,预防性技术的基础就是限制网站 frame 的能力。比较常见的客户端保护措施就是使用 web 浏览器的 frame 拦截或清理脚本,比如浏览器的插件或扩展程序,这些脚本通常是精心设计的,以便执行以下部分或全部行为:

  • 检查并强制当前窗口是主窗口或顶部窗口
  • 使所有 frame 可见。
  • 阻止点击可不见的 frame
  • 拦截并标记对用户的潜在点击劫持攻击。

Frame 拦截技术一般特定于浏览器和平台,且由于 HTML 的灵活性,它们通常也可以被攻击者规避。由于这些脚本也是 JavaScript ,浏览器的安全设置也可能会阻止它们的运行,甚至浏览器直接不支持 JavaScript 。攻击者也可以使用 HTML5 iframe 的 sandbox 属性去规避 frame 拦截。当 iframe 的 sandbox 设置为 allow-forms 或 allow-scripts,且 allow-top-navigation 被忽略时,frame 拦截脚本可能就不起作用了,因为 iframe 无法检查它是否是顶部窗口:

<iframe id="victim_website" src="https://victim-website.com" sandbox="allow-forms"></iframe>

当 iframe 的 allow-forms 和 allow-scripts 被设置,且 top-level 导航被禁用,这会抑制 frame 拦截行为,同时允许目标站内的功能。

结合使用点击劫持与 DOM XSS 攻击

到目前为止,我们把点击劫持看作是一种独立的攻击。从历史上看,点击劫持被用来执行诸如在 Facebook 页面上增加“点赞”之类的行为。然而,当点击劫持被用作另一种攻击的载体,如 DOM XSS 攻击,才能发挥其真正的破坏性。假设攻击者首先发现了 XSS 攻击的漏洞,则实施这种组合攻击就很简单了,只需要将 iframe 的目标 URL 结合 XSS ,以使用户点击按钮或链接,从而执行 DOM XSS 攻击。

多步骤点击劫持

攻击者操作目标网站的输入可能需要执行多个操作。例如,攻击者可能希望诱骗用户从零售网站购买商品,而在下单之前还需要将商品添加到购物篮中。为了实现这些操作,攻击者可能使用多个视图或 iframe ,这也需要相当的精确性,攻击者必须非常小心。

如何防御点击劫持攻击

我们在上文中已经讨论了一种浏览器端的预防机制,即 frame 拦截脚本。然而,攻击者通常也很容易绕过这种防御。因此,服务端驱动的协议被设计了出来,以限制浏览器 iframe 的使用并减轻点击劫持的风险。

点击劫持是一种浏览器端的行为,它的成功与否取决于浏览器的功能以及是否遵守现行 web 标准和最佳实践。服务端的防御措施就是定义 iframe 组件使用的约束,然而,其实现仍然取决于浏览器是否遵守并强制执行这些约束。服务端针对点击劫持的两种保护机制分别是 X-Frame-Options 和 Content Security Policy 。

X-Frame-Options

X-Frame-Options 最初由 IE8 作为非官方的响应头引入,随后也在其他浏览器中被迅速采用。X-Frame-Options 头为网站所有者提供了对 iframe 使用的控制(就是说第三方网站不能随意的使用 iframe 嵌入你控制的网站),比如你可以使用 deny 直接拒绝所有 iframe 引用你的网站:

X-Frame-Optionsdeny

或者使用 sameorigin 限制为只有同源网站可以引用:

X-Frame-Optionssameorigin

或者使用 allow-from 指定白名单:

X-Frame-Options: allow-from https://normal-website.com

X-Frame-Options 在不同浏览器中的实现并不一致(比如,Chrome 76 或 Safari 12 不支持 allow-from)。然而,作为多层防御策略中的一部分,其与 Content Security Policy 结合使用时,可以有效地防止点击劫持攻击。

Content Security Policy

Content Security Policy (CSP) 内容安全策略是一种检测和预防机制,可以缓解 XSS 和点击劫持等攻击。CSP 通常是由 web 服务作为响应头返回,格式为:

Content-Security-Policypolicy

其中的 policy 是一个由分号分隔的策略指令字符串。CSP 向客户端浏览器提供有关允许的 Web 资源来源的信息,浏览器可以将这些资源应用于检测和拦截恶意行为。

有关点击劫持的防御,建议在 Content-Security-Policy 中增加 frame-ancestors 策略。

  • frame-ancestors 'none' 类似于 X-Frame-Options: deny ,表示拒绝所有 iframe 引用。
  • frame-ancestors 'self' 类似于 X-Frame-Options: sameorigin ,表示只允许同源引用。

示例:

Content-Security-Policyframe-ancestors 'self';

或者指定网站白名单:

Content-Security-Policyframe-ancestors normal-website.com;

为了有效地防御点击劫持和 XSS 攻击,CSP 需要进行仔细的开发、实施和测试,并且应该作为多层防御策略中的一部分使用。

原文链接:https://segmentfault.com/a/1190000039341244

收起阅读 »

Web 安全 之 Directory traversal

Directory traversal - 目录遍历在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。什么是目录遍历?目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程...
继续阅读 »

Directory traversal - 目录遍历

在本节中,我们将介绍什么是目录遍历,描述如何执行路径遍历攻击和绕过常见障碍,并阐明如何防止路径遍历漏洞。


什么是目录遍历?

目录遍历(也称为文件路径遍历)是一个 web 安全漏洞,此漏洞使攻击者能够读取运行应用程序的服务器上的任意文件。这可能包括应用程序代码和数据、后端系统的凭据以及操作系统相关敏感文件。在某些情况下,攻击者可能能够对服务器上的任意文件进行写入,从而允许他们修改应用程序数据或行为,并最终完全控制服务器。

通过目录遍历读取任意文件

假设某个应用程序通过如下 HTML 加载图像:

![](/loadImage?filename=218.png)

这个 loadImage URL 通过 filename 文件名参数来返回指定文件的内容,假设图像本身存储在路径为 /var/www/images/ 的磁盘上。应用程序基于此基准路径与请求的 filename 文件名返回如下路径的图像:

/var/www/images/218.png

如果该应用程序没有针对目录遍历攻击采取任何防御措施,那么攻击者可以请求类似如下 URL 从服务器的文件系统中检索任意文件:

https://insecure-website.com/loadImage?filename=../../../etc/passwd

这将导致如下路径的文件被返回:

/var/www/images/../../../etc/passwd

../ 表示上级目录,因此这个文件其实就是:

/etc/passwd

在 Unix 操作系统上,这个文件是一个内容为该服务器上注册用户详细信息的标准文件。

在 Windows 系统上,..\ 和 ../ 的作用相同,都表示上级目录,因此检索标准操作系统文件可以通过如下方式:

https://insecure-website.com/loadImage?filename=..\..\..\windows\win.ini

利用文件路径遍历漏洞的常见障碍

许多将用户输入放入文件路径的应用程序实现了某种应对路径遍历攻击的防御措施,然而这些措施却通常可以被规避。

如果应用程序从用户输入的 filename 中剥离或阻止 ..\ 目录遍历序列,那么也可以使用各种技巧绕过防御。

你可以使用从系统根目录开始的绝对路径,例如 filename=/etc/passwd 这样直接引用文件而不使用任何 ..\ 形式的遍历序列。

你也可以嵌套的遍历序列,例如 ....// 或者 ....\/ ,即使内联序列被剥离,其也可以恢复为简单的遍历序列。

你还可以使用各种非标准编码,例如 ..%c0%af 或者 ..%252f 以绕过输入过滤器。

如果应用程序要求用户提供的文件名必须以指定的文件夹开头,例如 /var/www/images ,则可以使用后跟遍历序列的方式绕过,例如:

filename=/var/www/images/../../../etc/passwd

如果应用程序要求用户提供的文件名必须以指定的后缀结尾,例如 .png ,那么可以使用空字节在所需扩展名之前有效地终止文件路径并绕过检查:

filename=../../../etc/passwd%00.png

如何防御目录遍历攻击

防御文件路径遍历漏洞最有效的方式是避免将用户提供的输入直接完整地传递给文件系统 API 。许多实现此功能的应用程序部分可以重写,以更安全的方式提供相同的行为。

如果认为将用户输入传递到文件系统 API 是不可避免的,则应该同时使用以下两层防御措施:

  • 应用程序对用户输入进行严格验证。理想情况下,通过白名单的形式只允许明确的指定值。如果无法满足需求,那么应该验证输入是否只包含允许的内容,例如纯字母数字字符。
  • 验证用户输入后,应用程序应该将输入附加到基准目录下,并使用平台文件系统 API 规范化路径,然后验证规范化后的路径是否以基准目录开头。

下面是一个简单的 Java 代码示例,基于用户输入验证规范化路径:

File file = new File(BASE_DIRECTORY, userInput);
if (file.getCanonicalPath().startsWith(BASE_DIRECTORY)) {
// process file
}

原文链接:https://segmentfault.com/a/1190000039307155


收起阅读 »