Mooory

微信小程序开发小记

目录结构

根目录生成:项目配置文件project.config.json。setting属性下包含:

配置层

小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。

  • app有 app.js / app.json / app.wxss 文件。
  • 小程序页面有 js/wxml/json/wxss 文件。

app.json:文件用来对微信小程序进行全局配置,决定页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
(pages, window, tabBar, networkTimeout)
普通json:页面的配置只能设置 app.json 中部分 window 配置项的内容,页面中配置项会覆盖 app.json 的 window 中相同的配置项。

逻辑层

小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如 window,document 等。
app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
App({
app.js: App({onLaunch(options) {
// Do something initial when launch.
},
onShow(options) {
// Do something when show.
},
onHide() {
// Do something when hide.
},
onError(msg) {
console.log(msg)
},
globalData: 'I am global data'
})

对于小程序,可以在 App 的 onLaunch 和 onShow,或wx.getLaunchOptionsSync 中获取场景值。
wx.getLaunchOptionsSync(path, scene, query, shareTicker, refererInfo) 获取小程序启动时的参数。

普通js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// index.js
Page({
data: {
text: 'This is page data.'
},
onLoad(options) {
// Do some initialize when page load.
},
onReady() {
// Do something when page ready.
},
onShow() {
// Do something when page show.
},
onHide() {
// Do something when page hide.
},
onUnload() {
// Do something when page close.
},
onPullDownRefresh() {
// Do something when pull down.
},
onReachBottom() {
// Do something when page reach bottom.
},
onShareAppMessage() {
// return custom share data when user share.
},
onPageScroll() {
// Do something when page scroll
},
onResize() {
// Do something when page resize
},
onTabItemTap(item) {
console.log(item.index)
console.log(item.pagePath)
console.log(item.text)
},
// Event handler.
viewTap() {
this.setData({
text: 'Set some data for updating view.'
}, function () {
// this is setData callback
})
},
customData: {
hi: 'MINA'
}
})

生命周期:
onLoad() 页面加载时触发,只会调用一次,可获取当前页面路径中的参数。
onShow() 页面显示/切入前台时触发,一般用来发送数据请求;
onReady() 页面初次渲染完成时触发, 只会调用一次,代表页面已可和视图层进行交互。
onHide() 页面隐藏/切入后台时触发, 如底部 tab 切换到其他页面或小程序切入后台等。
onUnload() 页面卸载时触发,如redirectTo或navigateBack到其他页面时。

页面路由

Tab 切换:页面全部出栈,只留下新的 Tab 页面
初始化:新页面入栈
打开新页面:新页面入栈
页面重定向:当前页面出栈,新页面入栈
页面返回:页面不断出栈,直到目标返回页

getCurrentPages() 函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。

navigateTo, redirectTo 只能打开非 tabBar 页面。
switchTab 只能打开 tabBar 页面。
reLaunch 可以打开任意页面。
页面底部的 tabBar 由页面决定,即只要是定义为 tabBar 的页面,底部都有 tabBar。
调用页面路由带的参数可以在目标页面的onLoad中获取

wx.navigateTo 或使用组件 <navigator open-type="navigateTo"/>
url: 类型是String 跳转非 tabBar 的页面的路径 , 路径后可以带参数。参数与路径之间使用?分隔,参数键与参数值用=相连,不同参数用&分隔;如 ‘path?key=value&key2=value2’
success: 类型Function 接口调用成功的回调函数
fail: 类型Function 接口调用失败的回调函数
complete: 类型Function 接口调用结束的回调函数(调用成功、失败都会执行)

1
2
3
4
5
6
wx.navigateTo({
url: 'test?id=1'
})
wx.switchTab({
url: '/index' //不能带参数
})

模块化

使用 require(path) 将公共代码引入
module.exports 导出

且有模板:

1
2
3
4
5
6
<template name="head">
<view class='head-container'>
{{ info }}
<image src='../../images/icons/back.png' class='back-icon' wx:if="{{hasBack}}" bindtap='toBack'></image>
</view>
</template>

使用模板:

1
2
3
<import src="../../components/head.wxml"/>

<template is="head" data="{{info:'灯光设置', hasBack: true}}"/>

C import B,B import A,在C中可以使用B定义的template,在B中可以使用A定义的template,但是C不能使用A定义的template。
include 可以将目标文件除了 <template/> <wxs/> 外的整个代码引入,相当于是拷贝到 include 位置

1
2
3
4
<!-- index.wxml -->
<include src="header.wxml" />
<view>body</view>
<include src="footer.wxml" />

wxs:(像computer方法)
是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
<wxs module="m1">
var getMax = function(array) {
var max = undefined;
for (var i = 0; i < array.length; ++i) {
max = max === undefined ? array[i] : (max >= array[i] ? max : array[i]);
}
return max;
}
module.exports.getMax = getMax;
</wxs>

<!-- 调用 wxs 里面的 getMax 函数,参数为 page.js 里面的 array -->
<view>{{m1.getMax(array)}}</view>

在.wxs模块中引用其他 wxs 文件模块,可以使用 require 函数。

  • 只能引用 .wxs 文件模块,且必须使用相对路径。
  • wxs 模块均为单例,wxs 模块在第一次被引用时,会自动初始化为单例对象。多个页面,多个地方,多次引用,使用的都是同一个 wxs 模块对象。
  • 如果一个 wxs 模块在定义之后,一直没有被引用,则该模块不会被解析与运行。

存储与请求

1
2
wx.setStorageSync('key', 'value')  //同步
wx.getStorageSync('key')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
wx.request({
url: apiRoot + options.url,
data: options.data,
method: options.method ? options.method : 'POST',
header: {
'Cache-Control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded',
'token': token,
'XX-Device-Type': 'wxapp',
'XX-Api-Version': this.API_VERSION
},
success: res => {

},
fail: function (res) {

},
complete: options.complete ? options.complete : null
});

wx.login({
success: loginRes => {
if (loginRes.code) { //开发者需要在开发者服务器后台调用 code2Session,使用 code 换取 openid 和 session_key 等信息
wx.getUserInfo({
withCredentials: true,
success: res => {
const userInfo = res.userInfo
const nickName = userInfo.nickName
const avatarUrl = userInfo.avatarUrl
const gender = userInfo.gender // 性别 0:未知、1:男、2:女
const province = userInfo.province
const city = userInfo.city
const country = userInfo.country
},
fail: (res) => {

}
});


} else {
}
},
fail: () => {
}
})

开发者可以使用 wx.getSetting 获取用户当前的授权状态。
wx.authorize(Object object) //提前向用户发起授权请求。
wx.getUserInfo使用前要获取授权。

如何封装数据请求:
在根目录下创建utils目录及api.js文件和apiConfig.js文件;
在apiConfig.js 封装基础的get, post 和 put, upload等请求方法,设置请求体,带上token和异常处理等;
在api中引入apiConfig.js封装好的请求方法,根据页面数据请求的urls, 设置对应的方法并导出;
在具体的页面中导入。

小程序只可以跟指定的域名与进行网络通信。包括普通 HTTPS 请求(wx.request)、上传文件(wx.uploadFile)、下载文件(wx.downloadFile) 和 WebSocket 通信(wx.connectSocket)

  • 域名只支持 https

视图wxss

和css一样,都是用来描述页面的样子;
WXSS 具有 CSS 大部分的特性,也做了一些扩充和修改;
WXSS新增了尺寸单位,WXSS 在底层支持新的尺寸单位 rpx;
WXSS 仅支持部分 CSS 选择器;
WXSS 提供全局样式与局部样式。

节点信息获取

节点信息查询 API 可以用于获取节点属性、样式、在界面上的位置等信息。
最常见的用法是使用这个接口来查询某个节点的当前位置,以及界面的滚动位置。

1
2
3
4
5
6
7
8
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect(function (res) {
res.top // #the-id 节点的上边界坐标(相对于显示区域)
})
query.selectViewport().scrollOffset(function (res) {
res.scrollTop // 显示区域的竖直滚动位置
})
query.exec()

原理

小程序本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口;
它的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现;
它从技术讲和现有的前端开发差不多,采用JavaScript、WXML、WXSS三种技术进行开发;
功能可分为webview和appService两个部分;
webview用来展现UI,appService有来处理业务逻辑、数据及接口调用;
两个部分在两个进程中运行,通过系统层JSBridge实现通信,实现UI的渲染、事件的处理等。

架构:
微信小程序的框架包含两部分View视图层、App Service逻辑层,View层用来渲染页面结构,AppService层用来逻辑处理、数据请求、接口调用,它们在两个进程(两个Webview)里运行。
小程序启动时会从CDN下载小程序的完整包。

实现:
UI视图和逻辑处理是用多个webview实现的,逻辑处理的JS代码全部加载到一个Webview里面,称之为AppService,整个小程序只有一个,并且整个生命周期常驻内存,而所有的视图(wxml和wxss)都是单独的Webview来承载,称之为AppView。所以一个小程序打开至少就会有2个webview进程,正式因为每个视图都是一个独立的webview进程。

AppService:
可以理解AppService即一个简单的页面,主要功能是负责逻辑处理部分的执行,底层提供一个WAService.js的文件来提供各种api接口,主要是以下几个部分:
消息通信封装为WeixinJSBridge(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
1、日志组件Reporter封装
2、wx对象下面的api方法
3、全局的App,Page,getApp,getCurrentPages等全局方法
4、还有就是对AMD模块规范的实现
然后整个页面就是加载一堆JS文件,包括小程序配置config,上面的WAService.js(调试模式下有asdebug.js),剩下就是我们自己写的全部的js文件,一次性都加载。

上线后是应用部分会打包为2个文件,名称app-config.json和app-service.js,然后微信会打开webview去加载。线上部分应该是微信自身提供了相应的模板文件

AppView:
可以理解为h5的页面,提供UI渲染,底层提供一个WAWebview.js来提供底层的功能:
消息通信封装为WeixinJSBridge(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
2、日志组件Reporter封装
3、wx对象下的api,这里的api跟WAService里的还不太一样,有几个跟那边功能差不多,但是大部分都是处理UI显示相关的方法
4、小程序组件实现和注册
5、VirtualDOM,Diff和Render UI实现
6、页面事件触发

通信:
使用消息publish和subscribe机制实现两个Webview之间的通信,实现方式就是统一封装一个WeixinJSBridge对象,而不同的环境封装的接口不一样。

web-view页面分享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<web-view src="https://mp.weixin.qq.com/" bindmessage"bindGetMsg"></web-view>

//向小程序发送消息,会在特定时机(小程序后退、组件销毁、分享)触发组件的message事件
wx.miniProgram.postMessage({
data:'foo'
})
wx.miniProgram.postMessage({
data: {foo: 'bar'}
})
bindGetMsg(e) {
// e.detail = { data },data是多次 postMessage 的参数组成的数组
// e.detail.data[0]
let options = e.detail.data[0]
this.setData({
w_title: options.goods_name,
img: util.httpUrl + options.img,
share_url: '/pages/detailed/detailed?team_id=' + options.team_id + '&goods_id=' + options.goods_id
})
}
//分享的时候获取这些data
onShareAppMessage: function () {
return {
title: this.data.w_title,
imageUrl: this.data.img,
path: this.data.share_url
}
}
Continue
无重复字符的最长子串问题

##问题:

给定一个字符串,找出不含有重复字符的最长子串的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 无重复字符的最长子串是 “abc”,其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 无重复字符的最长子串是 “b”,其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 无重复字符的最长子串是 “wke”,其长度为 3。
请注意,答案必须是一个子串,”pwke” 是一个子序列 而不是子串。

##思考:
采用动态的划窗来查找无重复子串,用noRepeatHead指示当前划窗的起始位置,noRepeatEnd指示当前划窗的下一个需要验证的元素,当前不重复的字符组成的划窗子串为initialString。
初始时:

查找noRepeatEnd对应的元素是否在initialString中存在,若不存在,划窗自动加上这个元素,然后变成下面的状态:

若元素已经存在,直接截取从重复元素到当前元素的字符串,保持整个字符串中字符不重复。
(因为之后的字符串可能更长,比如‘abcbedg’, 原先的initialString为abc, 第四个字符‘b’在initialString中,直接截取‘cb’,再往后判断,因为往后可能有子串比目前的最大长度3还要大,例如‘cbedg’的长度5)
因为截取后长度肯定不大于之前noRepeatHead和noRepeatEnd之间的长度,所以直接不判断temp(当前initialString的长度)和返回值(nowMaxLength)的大小

直到noRepeatHead到达字符串末尾结束(下图 noRepeatEnd == s.length 了):

或者从noRepeatHead到字符串末尾的长度已经不能超过当前的最大长度(nowMaxLength)了,自然就不关心是否要继续判断:
noRepeatHead = 5,  s.length = 8, nowMaxLength = 3, 所以最后三个就不用判断啦

##代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @param {string} s
* @return {number}
*/
var lengthOfLongestSubstring = function(s) {
var result = 0;
if(s.length <= 1) {
return s.length;
}
var nowMaxLength = 1, noRepeatHead = 0, noRepeatEnd = 1;
var initialString = s[0]; var temp = 1, index = -1;
do {
index = initialString.indexOf(s[noRepeatEnd]);
if(index == -1) {
temp++;
if(temp>nowMaxLength){
nowMaxLength = temp;
}
initialString += s[noRepeatEnd];
noRepeatEnd ++;
} else {
noRepeatHead = noRepeatHead+ index + 1;//重复的话就把头移到重复元素的下一个元素上;
noRepeatEnd ++;
initialString = s.substring(noRepeatHead, noRepeatEnd);
temp = initialString.length;
}
}while(noRepeatHead < (s.length - nowMaxLength) && (noRepeatEnd < s.length));
return nowMaxLength;
};

Continue
webpack的相关概念与使用

介绍

本章介绍了webpack和相关技术的核心概念与使用,以及相关的扩展:懒加载、tree shaking、代码分离、缓存的方法~

webpack是什么?

具体的介绍可见官网:https://www.webpackjs.com/concepts/

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

把所有依赖打包成一个或多个 bundle 文件,通过代码分割成单元片段并按需加载。

与 gulp 和 grunt的区别

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等。

grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程。

webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能。

所以总结一下:

  • 从构建思路来说
    gulp和grunt需要开发者将整个前端构建过程拆分成多个Task,并合理控制所有Task的调用关系
    webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工

  • 对于知识背景来说
    gulp更像后端开发者的思路,需要对于整个流程了如指掌 webpack更倾向于前端开发者的思路

其他类似的工具

同样是基于入口的打包工具还有以下几个主流的:

  • webpack
  • rollup
  • parcel

webpack适用于大型复杂的前端站点构建;rollup适用于基础库的打包,如vue、react;parcel适用于简单的实验性项目,他可以满足低门槛的快速看到效果

webpack 与 rollup

webpack 发起之初主要是为了解决以下两个问题:

  • 代码拆分(Code Splitting): 可以将应用程序分解成可管理的代码块,可以按需加载,这样用户便可快速与应用交互,而不必等到整个应用程序下载和解析完成才能使用,以此构建复杂的单页应用程序(SPA);
  • 静态资源(Static Assets): 可以将所有的静态资源,如 js、css、图片、字体等,导入到应用程序中,然后由 webpack 使用 hash 重命名需要的资源文件,而无需为文件 URL 增添 hash 而使用 hack 脚本,并且一个资源还能依赖其他资源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

/* harmony default export */ __webpack_exports__["default"] = (str => str);


/***/ })
/******/ ]);

把 export default str => str; 这段代码用 webpack 打包就会得到上面的结果。

这在以下的一些情境中就不太高效,需要寻求更好的解决方案:

需要 js 高效运行。因为 webpack 对子模块定义和运行时的依赖处理(webpack_require),不仅导致文件体积增大,还会大幅拉低性能;
项目(特别是类库)只有 js,而没有其他的静态资源文件,使用 webpack 就有点大才小用了,因为 webpack bundle 文件的体积略大,运行略慢,可读性略低。

rollup 相对 webpack 而言,要小巧、干净利落一些,但不具备 webpack 的一些强大的功能,如热更新,代码分割,公共依赖提取等。

webpack 核心概念

核心概念有:入口、输出、loader、plugins

  1. 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。
  2. output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。【基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中!!!!!!!】
  3. loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

    在 webpack 的配置中 loader 有两个目标:
    test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
    use 属性,表示进行转换时,应该使用哪个 loader。
    
1
2
3
4
5
6
module: {
rules: [{
test: /\.css$/,
use: ["style-loader", 'css-loader']
}]
},
  1. loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,【从打包优化和压缩,一直到重新定义环境中的变量】。插件接口功能极其强大,可以用来处理各种各样的任务。
    想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。

  2. 模式:通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化

    1
    2
    3
    module.exports = {
    mode: 'production'
    };

入口

向 entry 属性传入「文件路径(file path)数组」将创建“多个主入口(multi-main entry)”。在你想要多个依赖文件一起注入,并且将它们的依赖导向(graph)到一个“chunk”时,传入数组的方式就很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
const config = {
entry: './path/to/my/entry/file.js'
};

module.exports = config;

// 等同于

const config = {
entry: {
main: './path/to/my/entry/file.js'
}
};

或者使用对象语言:

1
2
3
4
5
6
const config = {
entry: {
app: './src/app.js',
vendors: './src/vendors.js'
}
};

分离了应用程序app和第三方库vendor入口,这告诉我们 webpack 从 app.js 和 vendors.js 开始创建依赖图。这些依赖图是彼此完全分离、互相独立的。vendors选项中的引用可以使用 CommonsChunkPlugin 从整体bundle中提取 vendor bundle,并把引用 vendor 的部分替换为 webpack_require() 调用。

多页面:

1
2
3
4
5
6
7
const config = {
entry: {
pageOne: './src/pageOne/index.js',
pageTwo: './src/pageTwo/index.js',
pageThree: './src/pageThree/index.js'
}
};

则每个键(key)会是 chunk 的名称,该值描述了 chunk 的入口起点。

输出

即使可以存在多个入口起点,但只指定一个输出配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 普通输出
const config = {
output: {
filename: 'bundle.js',
path: '/home/proj/public/assets'
}
};

module.exports = config;

//多页面输出
{
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].js',
path: __dirname + '/dist'
}
}

//其他选项
output: {
filename: '[name].js', //对应于entry里面生成出来的文件名
chunkFilename: 'lib/[name].js'//就是未被列在entry中,但有些场景需要被打包出来的文件命名配置。
// 比如按需加载(异步)模块的时候 require.ensure
// 不是在ensure方法中引入的模块,此属性不会生效,只能用CommonsChunkPlugin插件来提取
}

filename: 决定了每个输出 bundle 的名称。这些 bundle 将写入到 output.path 选项指定的目录下。

loaders

在你的应用程序中,有三种使用 loader 的方式:

配置(推荐):在 webpack.config.js 文件中指定 loader。
内联:在每个 import 语句中显式指定 loader。
CLI:在 shell 命令中指定它们。

1
2
// 内联方式
import Styles from 'style-loader!css-loader?modules!./styles.css';

使用 ! 将资源中的 loader 分开。分开的每个部分都相对于当前目录解析。

特性:

loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
loader 可以是同步的,也可以是异步的。
loader 运行在 Node.js 中,并且能够执行任何可能的操作。
loader 接收查询参数。用于对 loader 传递配置。
loader 也能够使用 options 对象进行配置。
除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
插件(plugin)可以为 loader 带来更多特性。
loader 能够产生额外的任意文件。

插件

插件目的在于解决 loader 无法实现的其他事。
在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
webpack 插件是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。

1
2
3
4
5
6
7
8
9
10
//ConsoleLogOnBuildWebpackPlugin.js
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => {
console.log("webpack 构建过程开始!");
});
}
}

webpack解析规则

resolver 是一个库(library),用于帮助找到模块的绝对路径。
resolver 帮助 webpack 找到 bundle 中需要引入的模块代码,这些代码在包含在每个 require/import 语句中。 当打包模块时,webpack 使用 enhanced-resolve 来解析文件路径。

使用 enhanced-resolve,webpack 能够解析三种文件路径:

  • 绝对路径:不用解析了
  • 相对路径:使用 import 或 require 的资源文件(resource file)所在的目录被认为是上下文目录(context directory)。在 import/require 中给定的相对路径,会添加此上下文路径(context path),以产生模块的绝对路径(absolute path)。
1
2
import "../src/file1";
import "./file2";
  • 模块路径:模块将在 resolve.modules 中指定的所有目录内搜索。 你可以替换初始模块路径,此替换路径通过使用 resolve.alias 配置选项来创建一个别名。如果路径指向一个文件:如果路径具有文件扩展名,则被直接将文件打包。否则,将使用 [resolve.extensions] 选项作为文件扩展名来解析,此选项告诉解析器在解析中能够接受哪些扩展名(例如 .js, .jsx)。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import "module";

    // webpack.config.js中配置
    resolve: {
    modules: [
    "node_modules",
    path.resolve(__dirname, "src")
    ],
    extensions: [".js", ".jsx", ".json", ".css", ".scss"],
    alias: {
    xyz$: "./dir/file.js"
    }//创建 import 或 require 的别名,来确保模块引入变得更简单。
    },

生产环境构建和manifest

source map:用来调试代码,生产环境下一般使用:
(none)(省略 devtool 选项) - 不生成 source map。这是一个不错的选择。
source-map - 整个 source map 作为一个单独的文件生成。它为 bundle 添加了一个引用注释,以便开发工具知道在哪里可以找到它。

runtime,以及伴随的 manifest 数据,主要是指:在浏览器运行时,webpack 用来连接模块化的应用程序的所有代码。
当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 “Manifest”,当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。

模块热替换

hmr————hot module replacement:会在应用程序运行过程中替换、添加或删除模块,而无需进行完全刷新。


代码变化后自动编译代码,有三种实现方式:
webpack’s Watch Mode / webpack-dev-derver / webpack-dev-middleware,后两者都需要在配置里面加入devServer配置项

第一种:package.json文件的script指令:”watch”: “webpack –watch”,但是需要刷新浏览器
第二种:webpack.config.js的devServer(开发服务器)修改,运行命令:”dev”: “webpack-dev-server –progress –hot”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+   devServer: {
+ contentBase: './dist', //在哪儿查找文件,把 dist 目录下的文件,作为可访问文件。
+ historyApiFallback: true, //当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html。
+ host: 'http://localhost:8055', //指定端口
+ hot: true //启用 webpack 的模块热替换特性, 需要webpack或者webpack-dev-server 指令添加 --hot 属性,
// 这样webpack.HotModuleReplacementPlugin 会自动添加
+ proxy: {
+ "/api": "http://localhost:3000", // 请求到 /api/users 现在会被代理到请求 http://localhost:3000/api/users
+ "/video": {
target: "http://localhost:3000", //请求到 /videos/users 现在会被代理到请求 http://localhost:3000/users
pathRewrite: {"^/video" : ""},
secure: false //默认为true, false为接受运行在 HTTPS 上,且使用了无效证书的后端服务器。
}
+ },
publicPath: "/assets/" //如果我们输出的文件名为bundle.js,可以在浏览器中通过 http://localhost:8080/assets/bundle.js 访问
+ }

第三种:是一个容器(wrapper),它可以把 webpack 处理后的文件传递给一个服务器(server)。 webpack-dev-server 在内部使用了它,同时,它也可以作为一个单独的包来使用,以便进行更多自定义设置来实现更多的需求。


启动HMR:

1
2
3
4
5
6
7
8
9
10
11
12
    devServer: {
contentBase: './dist',
+ hot: true
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Hot Module Replacement'
}),
+ new webpack.NamedModulesPlugin(), //以便更容易查看要修补(patch)的依赖。
+ new webpack.HotModuleReplacementPlugin()
],

原理:(参考:https://zhuanlan.zhihu.com/p/30669007)
首先要知道server端和client端都做了处理工作

  1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
  2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
  3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
  4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
  5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
  6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
  7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
  8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。

扩展

webpack的 hash 和 缓存

可以通过命中缓存,以降低网络流量,使网站加载速度更快,然而,如果我们在部署新版本时不更改资源的文件名,浏览器可能会认为它没有被更新,就会使用它的缓存版本。由于缓存的存在,当你需要获取新的代码时,就会显得很棘手。

  1. 使用输出文件的文件名 filename: ‘[name].[chunkhash].js’,但是我们不做修改,这个hash也会改变!!!!【因为webpack 在入口 chunk 中,包含了某些样板(boilerplate),特别是 runtime 和 manifest。】
  2. CommonsChunkPlugin 可以用于将模块分离到单独的文件中。然而 CommonsChunkPlugin 有一个较少有人知道的功能是,能够在每次修改后的构建结果中,将 webpack 的样板(boilerplate)和 manifest 提取出来。将第三方库(library)(例如 lodash 或 react)提取到单独的 vendor chunk 文件中,是比较推荐的做法,这是因为,它们很少像本地的源代码那样频繁修改。因此通过实现以上步骤,利用客户端的长效缓存机制,可以通过命中缓存来消除请求,并减少向服务器获取资源,同时还能保证客户端代码和服务器端代码版本一致。
  3. 当改变时,vendor也会改变,。这是因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。可以用 NamedModulesPlugin,将使用模块的路径,而不是数字标识符;或者 HashedModuleIdsPlugin,推荐用于生产环境构建。

webpack有各种hash值,包括每次项目构建hash,不同入口的chunkhash、文件的内容contenthash,这么多hash,它们有什么区别呢?

  • hash是跟整个webpack构建项目相关的,每次项目构建hash对应的值都是不同的,即使项目文件没有做“任何修改”。
  • chunkhash,从字面上就能猜出它是跟webpack打包的chunk相关的。具体来说webpack是根据入口entry配置文件来分析其依赖项并由此来构建该entry的chunk,并生成对应的hash值。不同的chunk会有不同的hash值。一般在项目中把公共的依赖库和程序入口文件隔离并进行单独打包构建,用chunkhash来生成hash值,只要依赖公共库不变,那么其对应的chunkhash就不会变,从而达到缓存的目的。
  • contenthash表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样。在项目中,通常做法是把项目中css都抽离出对应的css文件来加以引用。

tree shaking

通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。
新的 webpack 4 正式版本,扩展了这个检测能力,通过 package.json 的 “sideEffects” 属性作为标记,向 compiler 提供提示,表明项目中的哪些文件是 “pure(纯的 ES2015 模块)”,由此可以安全地删除文件中未使用的部分。

一个js文件两个方法:add()和square(),只有add()被使用了,square()就要被去除,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//package.json
{
"name": "your-project",
"sideEffects": false //如果所有代码都不包含副作用,我们就可以简单地将该属性标记为 false,来告知 webpack,
//它可以安全地删除未用到的 export 导出。
//比如polyfill,影响全局作用域,不提供export,这就是副作用
}
//或者
{
"name": "your-project",
"sideEffects": [
"./src/some-side-effectful-file.js"
]
}

通过如上方式,我们已经可以通过 import 和 export 语法,找出那些需要删除的“未使用代码”,然而,我们不只是要找出,还需要在 bundle 中删除它们。为此,我们将使用 -p(production) 这个 webpack 编译标记,来启用 uglifyjs 压缩插件。
从 webpack 4 开始,也可以通过 “mode” 配置选项轻松切换到压缩输出,只需设置为 “production”。

总结:

  • 使用 ES2015 模块语法(即 import 和 export)。
  • 在项目 package.json 文件中,添加一个 “sideEffects” 入口。
  • 引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。

代码分离

  1. 如果我们的项目有多个入口,入口之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。可以通过通过使用 CommonsChunkPlugin 来移除重复的模块。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
      const path = require('path');
    const webpack = require('webpack');
    const HTMLWebpackPlugin = require('html-webpack-plugin');

    module.exports = {
    entry: {
    index: './src/index.js',
    another: './src/another-module.js'
    },
    plugins: [
    new HTMLWebpackPlugin({
    title: 'Code Splitting'
    + }),
    + new webpack.optimize.CommonsChunkPlugin({
    + name: 'common' // 指定公共 bundle 的名称。
    + })
    ],
    output: {
    filename: '[name].bundle.js',
    path: path.resolve(__dirname, 'dist')
    }
    };

输出:

1
2
3
4
5
6
7
8
9
10
11
12
Hash: 70a59f8d46ff12575481
Version: webpack 2.6.1
Time: 510ms
Asset Size Chunks Chunk Names
index.bundle.js 665 bytes 0 [emitted] index
another.bundle.js 537 bytes 1 [emitted] another
common.bundle.js 547 kB 2 [emitted] [big] common
[0] ./~/lodash/lodash.js 540 kB {2} [built]
[1] (webpack)/buildin/global.js 509 bytes {2} [built]
[2] (webpack)/buildin/module.js 517 bytes {2} [built]
[3] ./src/another-module.js 87 bytes {1} [built]
[4] ./src/index.js 216 bytes {0} [built]

  1. 使用ExtractTextPlugin: 将css从主应用中分离

  2. 对于动态导入,第一种,也是优先选择的方式是,使用符合 ECMAScript 提案 的 import() 语法。
    第二种,则是使用 webpack 特定的 require.ensure。

    1
    2
    3
    4
    5
    import('react-image-crop').then(ReactCrop => {

    })

    // 可以搭配await一起使用, 但需要 babel 和 plugin syntax-dynamic-import

懒加载或者按需加载

先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。

上面的动态导入已经产生一个分离的代码块,不要没有交互使用懒加载,这样做并没有对我们有很多帮助,还会对性能产生负面影响。
最好是用户有操作交互后,再进行懒加载。

1
2
3
4
5
+   button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
+ var print = module.default;
+
+ print();
+ });

externals

如果我们想引用一个库,但是又不想让webpack打包,并且又不影响我们在程序中以CMD、AMD或者window/global全局等方式进行使用,那就可以通过配置externals。

使用React

1
import React from 'react';

配置externals

1
2
3
externals: {
"react": React
}

Continue
文件下载一二事

最近博主做的项目对文件下载的方式有需求,因此也整理了部分资料以供分享。

a标签

这是我们最熟悉的方式。但是对于浏览器可以默认打开的格式:pdf、图像等,就不是默认下载了。
所以我们可以使用html5新增的download属性,默认下载。

1
<a href='file.js'>file.js</a>

这种方式有两个缺点:
1、兼容性不好
2、href必须是同源地址,不然download属性不生效,也是默认打开。也就是跨域无效。

决定是否能下载取决于:

1
2
3
4
//表明文件类型
header( "Content-Type: video/mp4" );
//类型为下载,同时设置下载文件名
header("Content-Disposition: attachment;filename=qwe.mp4");

window.location.href

使用window.location.href = ‘’也可以获取资源。不能下载默认打开格式也是一样的,不过可以通过
window.open() 打开新页面

createObjectURL

Gecko 2.0 (Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)引入了对DOM window.URL.createObjectURL() 和 window.URL.revokeObjectURL() 方法的支持。这使得你可以创建用于引用任何数据的简单URL字符串,也可以引用一个包括用户电脑上的本地文件的DOM File对象。

createObjectURL和 revokeObjectURL

这个对象URL是一个标识File对象的字符串。每次你调用window.URL.createObjectURL(),就会产生一个唯一的对象URL,即使是你对一个已创建了对象URL的文件再次创建一个对象URL。每个创建了的对象URL必须要释放。当文档关闭时,它们会自动被释放。如果你的网页要动态使用它们,你需要显式调用 window.URL.revokeObjectURL()来释放它们。

1
var blobObject = new Blob(["I scream. You scream. We all scream for ice cream."]);

但是这种方式在IE存在问题,你们看ie的createObjectURL和chrome不一样,chrome带了域名,而ie不带域名。
blob:242CACD6-06D5-4145-A6DA-55DBE47409DB
blob:null/242CACD6-06D5-4145-A6DA-55DBE47409DB
所以要采用ie独有的方法window.navigator.msSaveOrOpenBlob 或者window.navigator.msSaveBlob。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//解决浏览器兼容性问题
if('msSaveOrOpenBlob' in navigator){
// Microsoft Edge and Microsoft Internet Explorer 10-11
window.navigator.msSaveOrOpenBlob(fileBlob, name);
}else{
// standard code for Google Chrome, Mozilla Firefox etc
let dom = document.createElement('a');
dom.download = name;
dom.style.display = 'none';
let url = URL.createObjectURL(fileBlob);
dom.href = url;
document.body.appendChild(dom);
dom.click();
URL.revokeObjectURL(url);
}

我运用到这个需求是我请求资源必须在header的token中携带自己的身份信息,所以我不能使用a标签进行请求。必须使用请求获取二进制blob数据。

Continue
大文件上传

使用React生成一个大文件上传功能,此功能我们调用了ant design 的ui。

1
2
3
4
5
6
7
<FileUpload multiple chunked threadNum={3}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">点击选择文件或将文件拖拽到这里</p>
<p className="ant-upload-hint">系统支持在线播放.mp4,.flv,.ogv,.webm格式视频,其他格式视频需要下载后才可观看。</p>
</FileUpload>

这个组件的参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
multiple: PropTypes.bool, //是否支持多选文件
chunkSize: PropTypes.number, //文件分块大小
threadNum: PropTypes.number, //并发分块数量
accept: PropTypes.string, //接受上传的文件类型
onChange: PropTypes.func, //上传完成后的回调
chunked: PropTypes.bool //是否分片上传
}

const FileUpload = ({ chunked , ...rest }) => (
chunked ? <LargeFileUpload {...rest} /> : <SmallFileUpload {...rest} />
)

我们有两种上传方式,大文件上传,小文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import React from 'react';
import PropTypes from 'prop-types';
import {
Upload,
Button,
Icon,
Modal
} from 'antd';

import {
uploadFile
} from '../../../unit/fetch';

const propTypes = {
multiple: PropTypes.bool,
accept: PropTypes.string,
onChange: PropTypes.func
}

const Dragger = Upload.Dragger;

const FILE_STATE = {
WAITING: Symbol(),
UPLOADING: Symbol(),
FINISHED: Symbol(),
ERRORED: Symbol()
};

class SmallFileUpload extends React.Component {
constructor({
multiple = false,
accept = ''
}) {
super();
this.state = {
multiple,
accept,
disabled: false,
fileList: []
};

this.toUpload = this.toUpload.bind(this);
this.submit = this.submit.bind(this);
this.onCompleted = this.onCompleted.bind(this);
}

onCompleted() {
if (typeof this.props.onChange === 'function') {
this.props.onChange({
success: this.state.fileList.filter(info => info.state === FILE_STATE['FINISHED']).map(info => {return {token: info.token, name: info.config.file.name}}),
total: this.state.fileList.length
});
}
}

toUpload(fileInfo) {
return new Promise(async (resolve) => {
try {
const { token } = await uploadFile({
file: fileInfo.config.file
});
fileInfo.token = token;
fileInfo.state = FILE_STATE['FINISHED'];
fileInfo.config.onSuccess();
} catch(e) {
fileInfo.state = FILE_STATE['ERRORED'];
fileInfo.config.onError();
}
resolve();
});
}

async submit() {
if (!this.state.fileList.length) {
Modal.info({
title: '提示',
content: (
<div>
<p>请选择文件后,再提交!</p>
</div>
)
});
return;
}
for (let fileInfo of this.state.fileList) {
if (fileInfo.state === FILE_STATE['WAITING']) {
fileInfo.state = FILE_STATE['UPLOADING'];
await this.toUpload(fileInfo);
this.onCompleted();
}
}
}

render() {
const draggerProps = {
multiple: this.state.multiple,
accept: this.state.accept,
disabled: this.state.disabled,
customRequest: (config) => {
let fileInfo = {
id: config.file.uid,
state: FILE_STATE['WAITING'],
token: null,
config
};
this.setState({
fileList: [...this.state.fileList, fileInfo],
disabled: !this.state.multiple
});
},
onRemove: file => {
const fileList = [...this.state.fileList.filter(info => info.id !== file.uid)];
let isCompleted = fileList.every(info => [FILE_STATE['FINISHED'], FILE_STATE['ERRORED']].includes(info.state));
this.setState({
fileList,
disabled: fileList.length && !this.state.multiple
}, () => {
isCompleted && this.onCompleted();
});
}
};

return (<div>
<Dragger {...draggerProps}>
{this.props.children ? this.props.children :
<div>
<p className="ant-upload-drag-icon"><Icon type="inbox" /></p>
<p className="ant-upload-text">点击选择文件或将文件拖拽到这里</p>
</div>}
</Dragger>
<div style={{marginTop: '10px'}}>
<Button ghost type="primary" size="small" onClick={this.submit}>上传</Button>
</div>
</div>);
}
}

SmallFileUpload.propTypes = propTypes;

export default SmallFileUpload;

使用了ant design 的upload 组件,使用customRequest自己写上传逻辑。
获得的config对象:

1
2
3
4
5
6
7
8
9
10
11
{
action: ""
data: {}
file: File {uid: "rc-upload-1551879565599-4", name: "面试.txt", lastModified: 1547794695512, lastModifiedDate: Fri Jan 18 2019 14:58:15 GMT+0800 (中国标准时间), webkitRelativePath: "", …}
filename: "file"
headers: {}
onError: ƒ (r,o)
onProgress: ƒ (t)
onSuccess: ƒ (r,o)
withCredentials: false
}

我们的文件对象:

我们通过fetch上传一个一个小文件, 如果文件太大呢?

我们需要将文件进行分片上传。为了保证文件上传的正确性,不被挟持。我们要传递给后台md5值。

MD5在论坛上、软件发布时经常用,是为了保证文件的正确性,防止一些人盗用程序,加些木马或者篡改版权,设计的一套验证系统。每个文件都可以用MD5验证程序算出一个固定的MD5码来。

我们使用了Spark-MD5 JS, 它的api我们使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var hexHash = SparkMD5.hash('Hi there'); //二进制hash

// 增量相加
var spark = new SparkMD5();
spark.append('Hi');
spark.append(' there');
var hexHash = spark.end(); // hex hash
var rawHash = spark.end(true);

// 传递二进制数据的话
var chunks = Math.ceil(file.size / chunkSize),
spark = new SparkMD5.ArrayBuffer(),
currentChunk = 0;
fileReader = new FileReader();
fileReader.onload = function (e) {
spark.append(e.target.result); // Append array buffer
currentChunk++;

if (currentChunk < chunks) {
loadNext();
} else {
console.log('finished loading');
console.info('computed hash', spark.end()); // Compute hash
}
};

var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
function loadNext() {
var start = currentChunk * chunkSize,
end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;

fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}

上传的接口我们采用了三个接口,一个是用户根据MD5值,文件大小和名字创建上传Token。
二再根据上传Token,order向文件继续追加内容。
三是大文件合并,参数是Token和全部patch的数量

同时,因为大文件上传缓慢,我们要获取上传进度值。使用了progress event。

Progress Events定义了与客户端服务器通信有关的事件。这些事件最早其实只针对XHR操作,但目前也被其它API借鉴。

loadstart:在接收到相应数据的第一个字节时触发。
progress:在接收相应期间持续不断触发。
error:在请求发生错误时触发。
abort:在因为调用abort()方法而终止链接时触发。
load:在接收到完整的相应数据时触发。
loadend:在通信完成或者触发error、abort或load事件后触

通过xhr.upload.onprogress获取上传进度,lengthComputable是一个表示进度信息是否可用的布尔值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
xhr.upload.onprogress = function(evt) {
// event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
}

//这是单文件上传
export function uploadFile(options, progressBarFunc) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
let url = address.upload.substring(0, address.upload.length - 1);
let formdata = new FormData();
formdata.append('file', options.file);
xhr.open('POST', url);
xhr.setRequestHeader('Auth-Token', getAccessToken());
xhr.upload.onprogress = function(evt) {
if(evt.lengthComputable) {
progressBarFunc && progressBarFunc(evt.loaded, evt.total);
}
};
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(JSON.parse(xhr.responseText))
}
}
};
xhr.send(formdata);
});
}

// 分块上传,用户根据MD5值,文件大小和名字创建上传Token。再根据上传Token,order向文件继续追加内容。
export function getUploadTokenAndProgress(option) {
return executeFetch(address.upload, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Auth-Token': getAccessToken()
},
body: objToQueryString(option).substring(1)
});
}

export function uploadFileThunk(options, progressBarFunc) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
let url = `${address.upload}/append`;
let formdata = new FormData();
formdata.append('uploadToken', options.uploadToken);
formdata.append('order', options.order);
formdata.append('file', options.file);
xhr.open('POST', url);
xhr.setRequestHeader('Auth-Token', getAccessToken());
xhr.upload.onprogress = function(evt) {
if(evt.lengthComputable) {
progressBarFunc && progressBarFunc(evt.loaded, evt.total);
}
};
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
if (xhr.status === 200) {
resolve();
} else {
reject(JSON.parse(xhr.responseText))
}
}
};
xhr.send(formdata);
});
}

// 大文件合并, 参数是Token和全部patch的数量
export function finishFileUpload(option) {
return executeFetch(`${address.upload}/complete`, {
method: 'PUT',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Auth-Token': getAccessToken()
},
body: objToQueryString(option).substring(1)
});
}

完整的生成调用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
import React from 'react';
import PropTypes from 'prop-types';
import { Upload, Button, Icon, Modal } from 'antd';

import { getUploadTokenAndProgress, uploadFileThunk, finishFileUpload } from '../../../unit/fetch';

const propTypes = {
multiple: PropTypes.bool,
chunkSize: PropTypes.number,
threadNum: PropTypes.number,
accept: PropTypes.string,
onChange: PropTypes.func
}

const Dragger = Upload.Dragger;

const FILE_STATE = {
PREPARING: Symbol(),
WAITING: Symbol(),
UPLOADING: Symbol(),
FINISHED: Symbol(),
ERRORED: Symbol()
};

class LargeFileUpload extends React.Component {
constructor({
multiple = false,
chunkSize = 5 * 1024 * 1024,
threadNum = 1,
accept = ''
}) {
super();
this.state = {
multiple,
chunkSize,
threadNum,
accept,
disabled: false,
fileList: [],
isPrepared: false
};

this.submit = this.submit.bind(this);
this.sliceFile = this.sliceFile.bind(this);
this.md5File = this.md5File.bind(this);
this.toUpload = this.toUpload.bind(this);
this.onCompleted = this.onCompleted.bind(this);

import('../../../unit/tool/MD5File').then(MD5WorkerUrl => {
this.MD5Worker = new Worker(MD5WorkerUrl);
this.MD5Worker.onmessage = ({data}) => {
const fileInfo = this.state.fileList.find( info => info.id === data.id );
if(!fileInfo) {
return ;
}
fileInfo.MD5 = data.hash;
fileInfo.state = FILE_STATE['WAITING'];
let isPrepared = false;
for (let info of this.state.fileList) {
if (info.state === FILE_STATE['PREPARING']) {
isPrepared = true;
break;
}
}
this.setState({ isPrepared });
};
})

}

onCompleted() {
if (typeof this.props.onChange === 'function') {
this.props.onChange({
success: this.state.fileList.filter(info => info.state === FILE_STATE['FINISHED']).map(info => {return {token: info.token, name: info.config.file.name}}),
total: this.state.fileList.length,
isCompleted: !this.state.fileList.some(info => ![FILE_STATE['FINISHED'], FILE_STATE['ERRORED']].includes(info.state))
});
}
}

//文件切片
sliceFile(file, chunkCount) {
let sliceRst = [];
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
for (let i = 0, start, end; i < chunkCount; i++) {
start = i * this.state.chunkSize;
end = Math.min(file.size, start + this.state.chunkSize);
sliceRst.push(blobSlice.call(file, start, end));
}
return sliceRst;
}

//文件MD5
md5File(fileInfo) {
this.MD5Worker.postMessage({
'fileChunk': this.sliceFile(fileInfo.config.file, fileInfo.chunkCount),
'id': fileInfo.id
});
}

toUpload(fileInfo) {
return new Promise(async (resolve, reject) => {
const successThunkArr = new Set();
const fileChunkArray = this.sliceFile(fileInfo.config.file, fileInfo.chunkCount);
const completeUploadingFile = async () => {
fileInfo.config.onProgress({percent: 100});
try{
const { token } = await finishFileUpload({
uploadToken: fileInfo.uploadToken,
patchSum: fileInfo.chunkCount
});
fileInfo.token = token;
fileInfo.state = FILE_STATE['FINISHED'];
fileInfo.config.onSuccess();
resolve();
} catch(e) {
failureUploadThunk();
}
};
const failureUploadThunk = () => {
fileInfo.state = FILE_STATE['ERRORED'];
fileInfo.config.onError();
resolve();
};
const successUploadThunk = curChunkIndex => {
successThunkArr.add(+curChunkIndex);
if (successThunkArr.size === fileInfo.chunkCount) {
completeUploadingFile();
} else if (successThunkArr.size < fileInfo.chunkCount) {
fileInfo.config.onProgress({percent: (successThunkArr.size / fileInfo.chunkCount).toFixed(2) * 100 });
curChunkIndex && uploadThunk(successThunkArr.size);
}
};
//chunkIndex从0开始
const uploadThunk = (currentSuccessThunkNum, chunkIndex) => {
return new Promise(async resolve => {
const curChunkIndex = chunkIndex + 1 ? chunkIndex : currentSuccessThunkNum + Math.min(this.state.threadNum, fileInfo.chunkCount) - 1;
let failureTimes = 0;
function toUploadThunk() {
return new Promise(async resolve => {
try {
await uploadFileThunk({
uploadToken: fileInfo.uploadToken,
order: curChunkIndex,
file: fileChunkArray[curChunkIndex]
});
successUploadThunk(curChunkIndex);
resolve();
} catch(e) {
failureTimes++;
if (failureTimes < 3) {
toUploadThunk();
} else {
//文件上传失败,结束上传
failureUploadThunk();
}
}
})
}

if(curChunkIndex >= fileInfo.chunkCount) {
return
}

try {
const fileSize = fileInfo.config.file.size;
const { uploadToken, thunkArr } = await getUploadTokenAndProgress({
md5: fileInfo.MD5,
fileSize,
fileName: encodeURIComponent(fileInfo.config.file.name)
});
fileInfo.uploadToken = uploadToken;
thunkArr.forEach( index => successThunkArr.add(+index) );

if (successThunkArr.has(+curChunkIndex)) { //当前要上传的分片已存在服务器中
successUploadThunk(curChunkIndex);
} else {
await toUploadThunk();
}
resolve()
} catch(e) {
failureUploadThunk();
}
})
};

await uploadThunk(0, 0);

for(let i = 1, len = Math.min(this.state.threadNum + 1, fileInfo.chunkCount); i < len; i++) {
uploadThunk(successThunkArr.size, i);
}
});
}

async submit() {
if(!this.state.fileList.length) {
Modal.info({
title: '提示',
content: (
<div>
<p>请选择文件后,再提交!</p>
</div>
)
});
return;
}
for(let fileInfo of this.state.fileList) {
if (fileInfo.state === FILE_STATE['WAITING']) {
fileInfo.state = FILE_STATE['UPLOADING'];
await this.toUpload(fileInfo);
}
}
this.onCompleted();
}

render() {
const draggerProps = {
multiple: this.state.multiple,
accept: this.state.accept,
disabled: this.state.disabled,
customRequest: config => {
let fileInfo = {
id : config.file.uid,
state: FILE_STATE['PREPARING'],
chunkCount: Math.ceil(config.file.size / this.state.chunkSize),
MD5: null,
uploadToken: null,
token: null,
config
};
console.log(config);
this.setState({
fileList: [...this.state.fileList, fileInfo],
disabled: !this.state.multiple,
isPrepared: true
}, this.onCompleted);
this.md5File(fileInfo);
},
onRemove: file => {
const fileList = [...this.state.fileList.filter(info => info.id !== file.uid)];
let isCompleted = !fileList.some(info => ![FILE_STATE['FINISHED'], FILE_STATE['ERRORED']].includes(info.state));
this.setState({
fileList,
disabled: fileList.length && !this.state.multiple
}, () => {
isCompleted && this.onCompleted();
});
}
};

return (<div>
<Dragger {...draggerProps}>
{this.props.children ? this.props.children :
<div>
<p className="ant-upload-drag-icon"><Icon type="inbox" /></p>
<p className="ant-upload-text">点击选择文件或将文件拖拽到这里</p>
</div>}
</Dragger>
<div style={{marginTop: '10px'}}>
<Button ghost type="primary" size="small" onClick={this.submit} loading={this.state.isPrepared}>上传</Button>
{ this.state.isPrepared && <span style={{marginLeft: '7px'}}>正在读取文件,请稍后!</span>}
</div>
</div>);
}
}

LargeFileUpload.propTypes = propTypes;

export default LargeFileUpload;

逻辑是:比如并发的线程是3,我们的文件分成9块传送,
第一次:向后台请求我们文件发送的token,以及已经上传成功的块的order假设是Set[0,1,3]。第0块已有跳过。
第二步:根据线程数对[1,2,3]order的块重新发送上传请求,第一个请求order:1已经存在服务器了,就把【当前已经存储的数目+并发线程-1】=2+3-1 = 4,order为4的进行传递; order为2的不存在,继续上传。
第三步:直到上传完成。

Continue
web worker 使用指南

JavaScript 语言采用的是单线程模型,也就是说,所有任务只能在一个线程上完成,一次只能做一件事。前面的任务没做完,后面的任务只能等着。随着电脑计算能力的增强,尤其是多核 CPU 的出现,单线程带来很大的不便,无法充分发挥计算机的计算能力。

Web Worker 是HTML5标准的一部分,这一规范定义了一套 API,它允许一段JavaScript程序运行在主线程之外的另外一个线程中。
Web Worker 规范中定义了两类工作线程,分别是专用线程Dedicated Worker和共享线程 Shared Worker,其中,Dedicated Worker只能为一个页面所使用,而Shared Worker则可以被多个页面所共享

Continue
Home Archives Tags About Search
Theme Nayo