Listentolife

简单就好

  • 首页
  • 标签
  • 分类
  • 归档

axios源码学习(一):通过axios有多少种方法可以发起请求?

发表于 2020-05-30 | 分类于 JavaScript , axios

花了两天时间读了一遍axios源码,终于理解了axios内部是怎么运行的,这期间学习到了很多,所以想分享一下。想了很久,决定用一些问题来作为引子,一点点的整理一下我对axios源码的理解。下面我会用到一些官方文档的用例来说明。

用过axios的童鞋应该都知道,axios有很多请求的api,用起来是非常方便的。那么大家知道axios有多少种方法方式可以发起请求吗?为什么这些方法方式都可以发起请求?内部是怎么实现的。

这里我会先整理一下有哪些方法api,然后最后来整理一下源码中是如果实现这些方法的。

axios发起请求的方法

我们现在说说有哪些方法。首先要说明一下,我们一般都是这样引入axios的:

1
import axios from 'axios'

axios()

axios()是可以直接作为函数调用的。本身就是一个请求函数,可以直接传入请求的url,请求数据及请求配置。

1
2
3
4
5
6
7
8
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});

其实内部相当于调用了createInstance(config)方法。

axios.create()

这个方法不是能直接发起请求的,而是创建一个Axios实例,然后在通过实例的request()方法或者其他实例上的方法去发起请求。

1
2
3
4
5
6
7
// Create an instance using the config defaults provided by the library
// At this point the timeout config value is `0` as is the default for the library
const instance = axios.create();

instance.get('/longRequest', {
timeout: 5000
});

axios[method]()

这个方法比较直观,根据请求的method,可以选择对应的请求方法,比如get请求可以使用axios.get,post请求可以使用axios.post。

1
2
3
4
5
6
7
8
axios.get('/user/12345')
.then(function (response) {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
});

这些方法其实都是封装了axios.request()方法的,只是把传入的参数中method固定下来。

new axios.Axios

这个也是用来创建axios实例的,然后再用axios实例去调方法实现调起请求。

axios.request()

request()是axios中最重要的方法之一,上面所有的调起请求的方式都是基于这个方法,也是我们下面学习源码最先解读的内容。

内部实现

关键函数request()的实现

其实request()方法的源码非常少,很容易理解。主要分成两个部分,第一部分是整理合并config,第二部分是将拦截器的函数入栈chain,然后用Promise来执行。

这两个部分虽然没有提到发请求,但显而易见的是用Promise来执行时肯定是发了请求的。我们一步步来解读。

首先,第一部分是整理合并config。在这里,请求的配置除了传参进来的config之外,还有在axios.defaults中的配置。所以,需要先拿到config,然后跟axios.defaults得到本次请求的配置。

request()方法的源码中,对config做了统一格式的处理。因为我们知道request()方法的传参其实支持不止一个。

1
2
3
4
5
6
7
8
9
10
11
12
// 第一种传参方式
axios.request({
url: '/api',
method: 'get'
// ...
})

// 第二种传参方式
axios.request('/api', {
method: 'get'
// ...
})

所以会先判断config是否是String类型,如果是,则判断为传入url,第二个传参才是config,否则第一个传参就是config。同时也要判断没有传config的可能

1
2
3
4
5
6
7
// axios.request 源码
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

然后就可以合并请求配置了。源码中使用了mergeConfig()这个方法来处理两个配置对象的合并。这个方法内部其实是针对axios的所有配置做了专门的处理。

1
config = mergeConfig(this.defaults, config);

合并配置方法mergeConfig()

这个方法传参只有两个,config1和config2。内部处理的流程是声明了三类配置属性的数组valueFromConfig2Keys, mergeDeepPropertiesKeys, defaultToConfig2Keys,然后针对这三类做针对性的浅拷贝,深拷贝,默认值处理,接着再处理除这三类配置属性外其他配置属性的赋值,最后返回最终完整配置对象。

从整个处理流程来说,我认为方法中主要处理的三类配置属性应该是比较主要的配置,而且因为各自配置的不同需要分别处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// axios mergeConfig 源码
/**
* valueFromConfig2Keys这个数组保存有需要从用户获得的配置项
* mergeDeepPropertiesKeys这个数组保存有需要深拷贝的配置项
* defaultToConfig2Keys这个数组保存有axios已有默认值的配置项
*/
var valueFromConfig2Keys = ['url', 'method', 'data'];
var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
var defaultToConfig2Keys = [
'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
'maxContentLength', 'maxBodyLength', 'validateStatus', 'maxRedirects', 'httpAgent',
'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
];

valueFromConfig2Keys这类比较简单,但需要用户提供的,直接赋值即可:

1
2
3
4
5
6
7
// axios mergeConfig 源码
// 利用utils.forEach处理需要从用户获得的配置项
utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
if (typeof config2[prop] !== 'undefined') {
config[prop] = config2[prop];
}
});

mergeDeepPropertiesKeys这类配置的值类型可能是对象,需要做深拷贝:

1
2
3
4
5
6
7
8
9
10
11
12
13
// axios mergeConfig 源码
// 利用utils.forEach处理可能需要深拷贝的配置项
utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) {
if (utils.isObject(config2[prop])) { // 如果config2配置项值为对象,则使用深拷贝赋值
config[prop] = utils.deepMerge(config1[prop], config2[prop]);
} else if (typeof config2[prop] !== 'undefined') { // 如果 config2配置项不为undefined,则直接赋值
config[prop] = config2[prop];
} else if (utils.isObject(config1[prop])) { // 以上判断完config2,就判断config1的配置值是否为Object,是着使用深拷贝赋值
config[prop] = utils.deepMerge(config1[prop]);
} else if (typeof config1[prop] !== 'undefined') { // 否则判断不为undefined就直接赋值
config[prop] = config1[prop];
}
});

defaultToConfig2Keys这类配置是已有默认配置,所以可以使用默认值赋值。但内部还是做了undefined的判断,因为axios.default的配置值用户是可以修改的:

1
2
3
4
5
6
7
8
9
// axios mergeConfig 源码
// 利用utils.forEach来处理其他axios已有默认值的配置项
utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
if (typeof config2[prop] !== 'undefined') { // 如果config2中有配置且不为undefined,则赋值
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') { // 否则判断config1中配置不为undefined,则赋值
config[prop] = config1[prop];
}
});

最后的部分就是集合三个分类的配置属性,去筛选出不在这其中的用户配置属性,再去处理赋值,只做浅拷贝。这些配置可能用户也会在axios.default上设置默认值,所以也要考虑处理默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// axios mergeConfig 源码
// 合并所有前面已处理赋值的配置属性
var axiosKeys = valueFromConfig2Keys
.concat(mergeDeepPropertiesKeys)
.concat(defaultToConfig2Keys);

// 筛选出前面未处理到的用户配置属性
var otherKeys = Object
.keys(config2)
.filter(function filterAxiosKeys(key) {
return axiosKeys.indexOf(key) === -1;
});

// 处理前面未处理到的用户配置属性赋值
utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) {
if (typeof config2[prop] !== 'undefined') { // 判断config2中配置非undefined则赋值
config[prop] = config2[prop];
} else if (typeof config1[prop] !== 'undefined') { // 否则再判断config1中是否有默认非undefined值,有则赋值
config[prop] = config1[prop];
}
});

处理拦截器及请求

在这之前,先处理了请求的method,如果用户有传就将值改全小写,都没有就默认为get。

接下来的部分我认为很精巧。在调请求之前,因为axios还支持请求拦截器跟响应拦截器,所以这里使用类栈的方案。

先设置一个数组,这个数组默认有两个元素,一个是dispatchRequest,就是发送请求的方法,另一个是undefined。然后遍历请求拦截器,把其中的两个函数从数组的头部插入,再遍历响应拦截器,把两个函数从数组的尾部插入。这样一来,数组的结构就是从头到尾为就是按照请求的过程排好了请求拦截器一对对函数,然后是请求函数跟undefined,然后是响应拦截器一对对函数。

然后声明一个Promise.resolve()后返回的promise, 把数组中从头部开始一对对函数的传入promise.then()中。

这个操作可以利用promise.then()的特点,自动通过promise链去调用方法。如果请求拦截器中有报错,则最后不会调dispatchRequest方法并断掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// axios.request 源码
// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

最后返回这个promise使得我们调请求时可以用promise的方法继续处理。

其他请求方法的实现

那么request()这个方法是怎么演变出这么多种调用方式的呢?

首先,在Axios这个类下,提供了一堆axios[method]()的方法,作者是通过两个forEach来实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Axios 源码
// Provide aliases for supported request methods
// 这里提供了methods方法,是通过封装request来实现的,换言之所有逻辑都在request中
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});

然后引入的axios,实际是先调用了createInstance()方法,这个方法里声明一个instance的变量,通过bind方法得到了一个上下文为Axios实例的Axios.prototype.request()方法,最后返回instance。所以我们可以直接调用axios方法发请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// axios 源码
function createInstance(defaultConfig) {
// 声明一个Axios实例
var context = new Axios(defaultConfig);
// 返回一个function,this指向context,即Axios实例
// 相当于instance(args)会变成Axios.prototype.request.apply(context, args)
var instance = bind(Axios.prototype.request, context);

// Copy axios.prototype to instance
// 把Axios.prototype上的属性方法复制到instance上,this指向context
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
// 把context复制到instance
utils.extend(instance, context);

// 返回的instance上有Axios类上的方法,有Axios实例上所有属性跟方法
return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

而axios.create()方法内部就是直接使用了createInstance(), axios.Axios就是直接暴露Axios类。

结尾

关于axios请求的源码分析就整理完了,下一篇会来了解一下axios是怎么实现请求取消的。

webpack4配置总结(六)

发表于 2020-05-30 | 分类于 JavaScript , webpack

上一篇整理了plugins的配置。这篇开始整理webpack其他的一些配置项。

resolve

resolve是指定webpack打包构建过程中模块如何解析的相关配置。比如代码中的模块路径怎么解析,文件名怎么解析。这里简单整理一下两个配置项extensions及alias。

extensions

extensions是用来解析文件名的。我们一般会在引用文件是省略掉一些文件的扩展名,那么配置这一项可以告诉webpack,在解析省略扩展名的文件时,去匹配什么扩展名。这个配置项接受一个数组,这个数组列举了需要匹配的扩展名。匹配上顺序是数组从左到右,即优先匹配数组左侧的元素。如配置了['.js', '.vue'],则当遇到/src/main这个文件名时,会先在src目录下匹配是否存在main.js文件,有则解析这个文件,没有则继续匹配判断是否存在main.vue文件。

1
2
3
resolve: {
extensions: ['.js', '.vue'], // 自动解析确定的扩展名
},

alias

alias是用来创建import或require的别名,换言之,我们可以通过别名来压缩长路径。比如说,我们需要在文件中引入一个src目录下另一个目录中的文件,如果文件的层级很深,可能会写出import xxx from '../../../components/btn.vue'。通过配置别名,我们可以把@作为src目录的别名,这样就可以把前面的代码改写成import xxx from '@/components/btn.vue'。

1
2
3
4
5
6
7
8
9
10
resolve: {
alias: { // 配置模块引入中的别名
'vue': 'vue/dist/vue.esm.js',
'@': path.join(__dirname, "../src"),
'@images': path.join(__dirname, "../src/common/images"),
'@components': path.join(__dirname, "../src/components"),
'@common': path.join(__dirname, "../src/common"),
'@api': path.join(__dirname, "../src/api"),
}
},

devServer

devServer这个配置项是需要安装webpack-dev-server来启用的,用来配置本地服务器的。只要安装了webpack-dev-server就可以使用它来在本地启动服务器进行项目本地调试。不过还是有一些可配置的选项的。

port可以配置端口,默认是8080,如果有多个本地项目运行占用了8080端口,会自动后移端口号。compress为配置是否启用gzip压缩,可以提升返回页面的速度。contentBase为配置webpack启动在哪个目录下启动服务。historyApiFallback为配置是否启用HTML5 History API,当出现404响应时会被替代为index.html,这一项可以配置布尔值,也可以进一步配置选项。还有其他的选项可以到官网上查看。

1
2
3
4
5
6
7
devServer: {
// 开发服务的配置
// port: 8080, // 不配置默认为8080,如果同时有多个本地项目运行且占用8080端口,会自动改端口
compress: true, // 启用gzip压缩 可以提升返回页面的速度
contentBase: path.resolve(__dirname, '../dist'), // webpack 启动服务会在dist目录下
historyApiFallback: true // 当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html
}

optimization

optimization是webpack的优化配置项,从webpack4开始,用来根据不同的打包环境(设置mode)对打包进行优化,而且支持手动配置及重写的。optimization的可选配置项,有些是设置值,有些是通过引入插件来实现的,所以这里会结合实际情况来整理一些配置项的使用方法。

minimizer

minimizer支持用户自定义打包压缩的插件,并替换掉webpack默认的压缩插件。一方面,用户可以自行选择压缩js代码的插件,另一方面,项目中还有其他编程语言需要使用压缩插件。webpack打包压缩默认只压缩js代码,但是css代码并没有压缩。而如果指定了压缩css代码的插件,那么原有的压缩js代码的功能就会被覆盖掉,所以同时也需要指定压缩js代码的插件。这里就使用到了optimize-css-assets-webpack-plugin及terser-webpack-plugin。

optimize-css-assets-webpack-plugin

optimize-css-assets-webpack-plugin是用来压缩优化css代码的插件,默认使用cssnano来压缩优化。它接收5个配置项,分别是:assetnameRegExp(一个匹配文件的正则表达式),cssProcessor(自定义用于优化压缩css的处理器),cssProcessorOptions(传递给css处理器的选项), cssProcessorPluginOptions(传递给css处理器的插件选项)及canPrint(一个布尔值,指定是否可以将信息打印到控制台)。

terser-webpack-plugin

terser-webpack-plugin是用来压缩js代码的插件。因为配置了minimizer,覆盖了默认的压缩功能,所以也需要配置压缩js的插件。这个插件有比较多的配置项,不过不配置也可以达到最佳的优化效果,所以需要配置可以去看官方文档。

1
2
3
4
5
6
optimization: {
minimizer: [
new OptimizeCSSAssetsPlugin(), // 压缩css,但使用之后js文件也需要手动压缩
new TerserWebpackPlugin(), // 压缩js
]
},

runtimeChunk

runtimeChunk是用来优化持久化缓存的。从我的理解来说,runtimeChunk默认是false,意思是webpack打包构建之后,会各个模块之间的引用和加载的逻辑内嵌到模块中,如果某个模块发生改变时,会导致除了除此之外引用及加载的模块一并发生改变,导致缓存失败。

如果设置为true(也可以是一个object配置对象来表示启用)或者multiple,会在打包构建之后,给每个入口文件都生成一个对应的runtime文件,里面会存放有生成一个包含chunk映射关系的表。一旦某个模块发生改变时,只会修改该模块跟对应的runtime文档,不会影响到引用及加载的模块,实现持久化缓存。

如果设置single,则表示在打包构建之后,只生成一个runtime文件,存放项目所有chunk映射关系表。

如果是一个object配置对象,里面可以配置name来指定生成的runtime文件的命名。

splitChunks

splitChunks这个配置项是用来定义使用什么策略拆分打包模块。里面有非常多的可配置选项来制定你的拆分打包策略。下面来一个个的介绍配置项:

chunks

chunks用来指定哪些模块会应用优化。支持配置三个值all,async,initial。all是指同时拆分同步和异步代码,async是默认值,指只拆分异步代码,initial虽然也会同时拆分同步和异步代码,但是异步代码内容不会再拆分。

minSize

minSize用来指定文件超过minSize值则会启用抽离。

maxSize

maxSize用来指定拆分的模块最大尺寸,不过有些模块即使大小超过maxSize,但是拆分不一定是最佳选择。

minChunks

minChunks指定模块至少达到minChunks引用次数才会启用抽离。

maxAsyncRequests

maxAsyncRequests指定并行请求数量

maxInitialRequests

maxInitialRequests指定首屏并行请求数量

automaticNameDelimiter

automaticNameDelimiter指定拆分模块的命名的界定符。webpack默认拆分模块的命名会使用模块的来源和名称使用界定符来生成。默认界定符为~。

automaticNameMaxLength

automaticNameMaxLength指定拆分模块的命名的最大长度。

cacheGroups

cacheGroups用来自定义缓存组。cacheGroups接受一个对象,这个对象的键名为缓存组组名,值是一个对象,可以设置一系列相关配置。如果需要禁用默认缓存组,可以在cacheGroups下设置default: false。缓存组有以下配置项:

priority是用来设定缓存组的优先级。因为一个模块可以属于多个缓存组,所以模块会根据缓存组的优先级来选定放在哪个缓存组中。

reuseExistingChunk是用来指定当前模块包含从主模块中拆分出来的模块,是否重用该模块。

test是用来指定缓存组选择的模块,接受一个正则表达式来匹配模块名或路径。

filename指定缓存组模块的文件名占位符

enforce指定webpack是否忽略splitChunks.minSize,splitChunks.minChunks,splitChunks.maxAsyncRequests和splitChunks.maxInitialRequests,只创建缓存组模块。

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
optimization: {
splitChunks: {
chunks: 'all', // 'async' 默认支持异步的代码分割 如import()
minSize: 30000, // 文件超过30k,就会抽离
maxSize: 0,
minChunks: 1, // 最少模块引用1次才抽离
maxAsyncRequests: 6, // 最多6个请求
maxInitialRequests: 4, // 最多首屏加载4个请求
automaticNameDelimiter: '~', // 抽离文件名***~a~b,代表a,b都有引用,使用~来分隔
automaticNameMaxLength: 30, // 最长名字大小
cacheGroups: { // 缓存组
vues: {
test: /[\\/]node_modules[\\/]\/vue|vue-router|vuex/,
priority: 1 // 优先级
},
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 1, // 至少引用1次
priority: -20,
reuseExistingChunk: true
}
}
},
},

至此,整理完了我的模板项目的webpack配置。

这套webpack配置我也尝试改成基于React的项目,也能成功的跑通。当然,webpack的配置不一定每个项目都相同,都是有不同的需求跟优化的空间。目前我这个模板项目还是有一些地方可以再优化,或者在我更深入了解webpack之后可能又会有更好的配置。之后还有webpack5,也是以后需要去研究应用的内容。

webpack4配置总结(五)

发表于 2020-05-30 | 分类于 JavaScript , webpack

上一篇整理了babel跟postcss-loader的配置,结束了webpack的loader部分。这边开始整理plugins。

plugins

plugins插件是用来拓展webpack功能的。他们会在整个webpack打包构建过程中生效,执行相关的任务。插件与laoder不同,loader主要是处理某一些源文件的,用完就不需要了,但是插件是在一直作用于构建中。

使用插件时,需要先npm安装插件,然后在plugins属性下添加插件的实例。plugins属性接受的值是一个数组。

插件有很多,根据项目的需求使用的插件也不一样,不像loader那种可能比较固定。所以这里就结合项目需求来整理插件。

DefinePlugin

我们一般会需要在项目全局上设置一些常量,比如不同环境下,可能需要设定版本,不同的请求域名,可能需要标识常量来区分测试环境跟生产环境。但是在开发项目时,这些值可能需要在打包前根据打包的环境不同手动修改。如果常量不多还好,如果很多,容易改错,或者忘记修改,导致项目上线不正常。

DefinePlugin是webapck官方插件,这个插件允许创建一个在编译时可以配置的全局常量。这个插件就可以很好的解决这个问题。

这个插件允许用户自定义键值对,其中对键值有四个判断处理:

  1. 如果这个值是一个字符串,它会被当作一个代码片段来使用。
  2. 如果这个值不是字符串,它会被转化为字符串(包括函数)。
  3. 如果这个值是一个对象,它所有的 key 会被同样的方式定义。
  4. 如果在一个 key 前面加了 typeof,它会被定义为 typeof 调用。

使用时,直接使用常量即可,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack配置
const webpack = require('webpack');
let isDev = env.development;

plugins: [
new webpack.DefinePlugin({ // 可以在项目中拿到当前环境变量
'NODE_ENV': JSON.stringify(isDev ? 'dev' : 'prod'),
}),
]

// 项目中业务代码
if (NODE_ENV === 'dev') {
// ...
}

vue-loader plugin

当项目是基于Vuejs开发时,webpack就需要配置vue-loader plugin。通过官方文档可以知道,这个插件就是用来处理.vue文件中各个模块匹配不同loader。如前面.js文件配置的loader会应用到.vue文件中的script模块,.css文件,.scss文件配置的loader会应用到.vue文件中的style模块。

这个插件不是独立的,是配置vue-loader的时候同时需要使用到的,在vue-loader/lib/plugin目录下。

1
2
3
4
5
const VueLoaderPlugin = require('vue-loader/lib/plugin');

plugins: [
new VueLoaderPlugin(), // 作用是处理匹配.vue文件的rule
]

html-webpack-plugin

前面写了这么一系列的配置,但是有个关键的问题,就是打包后的文件会从哪里引用。因为是网页,所以一定需要用一个html文件来引用。但是手动创建一个比较麻烦。这里就可以使用html-webpack-plugin这个插件。

这个插件可以自动生成一个html文件,并使用script标签引入打包后的文件。

1
2
3
4
5
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
new HtmlWebpackPlugin(),
]

上面就是最简单的配置,打包之后会生成一个index.html。

这个插件也支持配置。详细的配置项可以通过官网查看。这里简单介绍几个配置项:

template用来配置生成index.html基于的模板,值是模板在项目中的相对路径或者绝对路径。

filename用来配置生成html文件的命名,默认为index.html。

minify用来配置在打包生产环境时生成的html文件压缩设置。这个配置项默认在生成环境下为true,也可以手动配置压缩哪些,比如去除空白,删除注释等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HtmlWebpackPlugin = require('html-webpack-plugin');
let isDev = env.development;

plugins: [
new HtmlWebpackPlugin({ // 自动生成html文件
template: path.resolve(__dirname, '../public/index.html'), // 配置生成HTML文件的模板
filename: 'index.html', // 生成html文件的命名
minify: !isDev && {
removeComments: !isDev, // 移除注释
removeAttributeQuotes: !isDev, // 移除引号
collapseWhitespace: !isDev // 折叠空白
}
}),
]

html-webpack-tags-plugin

我们在项目中不免会需要在html文件中直接引入一些文件,比如用来重写标签一般样式的css文件,项目中涉及微信api时需要引入相关的js文件。这些文件我们当然可以直接利用html-webpack-plugin,把需要引入文件写到html模板文件中,然后打包的时候直接生成。不过这样肯定有些人觉得不够优雅。那么就可以试试使用html-webpack-tags-plugin这个插件。

html-webpack-tags-plugin这个插件支持在打包的时候把配置文件自动引入到html文件中。这样我们就可以在webpack配置中对引入的文件一目了然。

html-webpack-tags-plugin也有很多配置项,我这里也只是对几个用上的配置项做介绍,其他配置项可以根据项目需求使用。

tags用来指定需要插入的文件列表。这个配置项接受一个数组,这个数据的元素可以是字符串,表示引入的文件地址,也可以是一个json格式,可以配置除了引入文件的地址path之外其他配置。如type指定文件类型,publicPath指定文件路径前是否需要应用publicPath。

append用来指定是否将tags中列出的文件在打包文件位置前引入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin');

plugins: [
new HtmlWebpackTagsPlugin({ // 在html文件中插入引用文件
tags: [
{
path: '//res.wx.qq.com/open/js/jweixin-1.4.0.js',
type: 'js',
publicPath: false, //不在路径面前应用publicPath
}
],
append: false, //插入顺序在打包文件前面
}),
]

clean-webpack-plugin

clean-webpack-plugin这个插件是用来在打包输出文件之前,清理打包输出目录文件的工具。一般我们配置的打包输出目录是固定的,每次打包如果不清除目录内的文件,会导致目录内的文件越来越多。在这种情况下,我们可以使用这个插件来清除。

这里有一个实际使用的问题。旧版本的clean-webpack-plugin不会清除隐藏文件及目录,但是新版本中会完全清除。这就导致出现一个问题。在实际业务中,打包目录有可能就是远程仓库对应的目录,那么目录中是会有被隐藏的.git目录。如果直接使用新版的插件,打包时也会把.git目录清除,导致无法使用推送代码。

我查了官方文档及很多博客,也没有找到比较合适的解决方案,但是还是找到了cleanOnceBeforeBuildPatterns这个配置项。这个配置项可以指定哪些文件需要清除的。它接受一个数组,数组的每一项都是一个正则。默认为全部清除。我就使用这个配置项来排除.git目录。最后是得到使用以下配置可以达到保留.git目录,其他文件清除的效果:

1
2
3
4
5
6
7
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

plugins: [
new CleanWebpackPlugin({ // 输出打包文件前清除旧的打包文件
cleanOnceBeforeBuildPatterns: ['**/*', '!\.git', '!\.git/**/*']
})
]

mini-css-extract-plugin

这个插件用来抽离css代码到一个单独的style文件。官方介绍有以下特点:1. 异步加载;2. 无重复编译;3. 使用简便;4. 针对css。

使用的时候,除了需要在plugins中new一个插件的实例,还需要在loader上配置对css打包上使用插件对应的loader。

mini-css-extract-plugin也有一些配置项,最常用的就是filename及chunkFilename,用来配置css文件的命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
let isDev = env.development;

{
module: {
rules: [
{
test: /\.css$/,
use: [
// ...
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 一般使用MiniCssExtractPlugin就不使用style-loader
// ...
]
},
]
},
plugins: [
!isDev && new MiniCssExtractPlugin({ // 将css单独打包成一个文件的插件,它为每个包含css的js文件都创建一个css文件。它支持css和sourceMaps的按需加载。
filename: 'main.css'
}),
].filter(Boolean)
}

webpack-bundle-analyzer

这个插件是用来分析打包文件的,目的是让打包的项目以图表的形式展示给开发者,可以直观的分析项目打包文件的情况,为优化做判断。配置这个插件后,在打包完成之后,会启动http服务器打开一个页面来显示打包文件的情况。

使用也比较简单,也有一些配置项,这里项目中没有用到,就简单略过:

1
2
3
4
5
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

plugins: [
new BundleAnalyzerPlugin() // 打包代码分析
]

接下来将整理剩下的webpack配置中剩下的部分。

webpack4配置总结(四)

发表于 2020-05-30 | 分类于 JavaScript , webpack

上一篇整理了loader的使用,这篇整理一下babel跟postcss-loader的配置。

.babelrc

插件跟预设

其实,.babelrc配置文件是用来支持我们在使用babel解析编译代码时,使用一些插件来处理高版本语法或者提案语法。插件跟预设的区别在于,预设其实是一些插件的组合,它方便我们处理一类语法的打包编译问题,不需要我们手动配置一系列插件。.babelrc里只需要写成一个JSON格式。

预设@babel/preset-env

按照官网的说法,@babel/preset-env是一个智能的预设,它可以解析编译最新的js语法。它可以根据目标浏览器环境情况编译合适的插件列表,使编译打包后的项目能在目标浏览器环境中正常运行。

而目标浏览器的罗列,可以在package.json中使用browserslist属性设置,也可以设置预设属性targets,还可以在项目根目录上创建.browserslistrc来设置,如:

1
2
// .browserslistrc
cover 95%

@babel/preset-env除了上面提到的targets配置项外,还有两个配置项是在我的项目中使用到的。一个是useBuiltIns,另一个是corejs,两个配置项有关联。

useBuiltIns有三个值,其中默认值为false,意思是不引入babel编译的结果,把引入的位置,引入那些polyfill交给用户处理。之前的做法是安装@bable/polyfill,然后在入口文件中引入,但其实这样会引入很多用不着的polyfill。

这里说明一下,polyfill在MDN上有定义解释,我这里就摘一下:

Polyfill 是一块代码(通常是 Web 上的 JavaScript),用来为旧浏览器提供它没有原生支持的较新的功能。
比如说 polyfill 可以让 IE7 使用 Silverlight 插件来模拟 HTML Canvas 元素的功能,或模拟 CSS 实现 rem 单位的支持,或 text-shadow,或其他任何你想要的功能。

如果设置值为entry,则会将文件中import '@babel/polyfilll'语句结合targets,转换为一系列引入语句,去掉目标浏览器已支持的polyfilll模块,不管代码里有没有用到,只要目标浏览器不支持都会引入对应的polyfilll 模块。虽然要比全部引入要好,但是使用不到的polyfill还是会有。

而且现在babel也废弃了@bable/polyfill,所以需要我们自己选择使用哪个兼容库。那么这里的useBuiltIns也就最好设置为usage。

设置usage,则不需要手动在代码中引入@babel/polyfill,而且打包的时候会根据实际代码的需要引入实际用上的polyfill模块。同时,因为@babel/polyfill已经不建议用,被废弃了,所以我们需要通过设置corejs来指明使用的那个版本的core-js。

其实@babel/polyfill是对core-js(core-js/stable)做了封装,而且引用了regenerator-runtime/runtime。
core-js其实是把所有新语法通过ES6之前的语法实现(现在已经支持到ES2019)。

这里可以设置为2或3,指定使用core-js哪个版本。关于core-js到底使用2还是3,怎么选择,原因是什么,我也还没有搞得很明白,网上关于这方面的说法不多,目前自己的项目中用的是2,而且版本是`core-js@2.6.11`,安装版本不同可能还会报错。所以这部分需要等以后了解了再整理出来。

所以最终预设的配置如下:

1
2
3
4
5
6
7
8
9
{
"presets": [ // 预设,从下到上执行
["@babel/preset-env", {
"useBuiltIns": "usage", // 使用的api 会自动转化 并且是按需加载
"corejs": 2 // 替换掉babel-polyfill,babel-polyfill已不建议使用
}]
],
// ...
}

插件

转回来我们来了解一下什么是插件。因为babel是一个编译器,虽然有解析,转换和打印输出三个步骤,但实际在转换上并不会做什么处理,也就是什么的不做就到打印输出了。但它支持插件,可以通过插件来进行代码转换。所以,所谓的插件,就是可以对babel解析的代码进行转换的编译工具(自己理解)。

配置插件,是通过plugins这个配置属性来配置的。plugins接受一个数组,这个数组用来列举插件。执行的顺序是从上到下。如果插件需要进一步配置,则写成数组。数组的第一项是插件名,第二项是配置json。

这里简单介绍一下我项目中使用到的插件:

@babel/plugin-proposal-decorators插件是用来解析支持装饰器草案语法的,按babel的举例演示,实际上是把装饰器语法转换为高级函数,用来返回类或者函数。这个插件有两个可选配置,一个是decoratorsBeforeExport,用来指定是否支持export语法前使用装饰器。值为true和false。另一个是legacy,用来指定是否支持旧式装饰器的语法和行为,默认值为false。

@babel/plugin-proposal-class-properties插件是用来解析支持class草案语法。这个插件有一个可选配置loose,这个配置默认为false,即

1
2
3
class A {
x = 1
}

中x会使用Object.defineProperty来严格定义。如果loose设置为true,则为宽松处理,x将直接使用赋值表达式来处理。

上面两个插件如果同时使用时,官网有对配置上有明确的说明,@babel/plugin-proposal-decorators必须要在@babel/plugin-proposal-class-properties前面调用,而且如果@babel/plugin-proposal-decorators的legacy设置了true,则要求@babel/plugin-proposal-class-properties的loose设置为true。

最后一个插件是@babel/plugin-transform-runtime。这个插件还需要安装@babel/runtime,是用例使用帮助函数来压缩代码。插件的功能是将编译后代码中把重复的代码块用过帮助函数来处理,使得代码量得以减少。

最后列一下配置:

1
2
3
4
5
6
7
{
"plugins": [ // 插件 从上到下执行
["@babel/plugin-proposal-decorators", {"legacy": true}], // 支持解析装饰器草案语法,legacy表示保留装饰器语法,需要@babel/plugin-proposal-class-properties的loose配置为true
["@babel/plugin-proposal-class-properties", {"loose": true}], // 支持解析class草案语法,loose表示宽松处理,即class A{a = 1}会转化为class A{this.a = 1}
"@babel/plugin-transform-runtime" // 需要安装@babel/runtime,使用帮助函数压缩代码
]
}

postcss.config.js

上篇说到,postcss是一个用来转化css代码的js工具,它支持配置插件来转化css代码,可以直接在webpack配置中配置option,也可以通过postcss.config.js来配置。

跟.babelrc的最外层写法不一样,postcss.config.js需要写成一个模块导出。里面的结构好像也不一样,只有一个plugins的配置项,这个配置项需要传入一个数组,这个数组的每一项就是需要使用的插件,通过require引入。

官方文档上列了很多插件,这里就不细细展开,只简单整理一下项目中使用的两个插件。

autoprefixer插件是用来添加浏览器前缀。这个插件方便我们在打包的时候生成不同浏览器不同前缀的样式。

postcss-pxtorem是用来把px单位转化为rem的插件。这个插件主要使用在移动端。移动端的屏幕尺寸有很多种,尺寸比例也各不相同。一般我们会使用rem的写法,只是一套经典的解决方案。这里使用postcss-pxtorem,配置的时候可以配置rootValue,用来指定一单位rem等于多少px。这样可以让你在开发时只要使用px,打包的时候会以375px视窗宽度换算宽度的rem值,然后项目上线后会以这个值来计算其他宽度的比例。比如rootValue设置37.5,则视窗宽度将固定为10rem,其他尺寸均以这个为标准。

这个插件让我们不需要关注怎么实现移动端屏幕适配问题。当然,如还有有些配置项还是需要注意的,如propList可以配置那些样式属性不需要转化,unitPrecision为转换成rem后保留的小数点位数,exclude为排除转化的目录。

还有一个问题,如果有需要不转化px,还可以写成PX。

最后,附上配置:

1
2
3
4
5
6
7
8
9
10
11
module.exports = {
plugins: [
require('autoprefixer'),
require('postcss-pxtorem')({
rootValue: 30.1932367, // body宽折算为12.42rem。如果这里设置37.5,则宽折算为10rem
unitPrecision: 5,
propList: ['*'],
// exclude: /node_modules/i
})
]
}

接下来将整理webpack配置中的插件plugins。

webpack4配置总结(三)

发表于 2020-05-30 | 分类于 JavaScript , webpack

上一篇整理了webpack配置的四大配置概念。这篇来整理loader的使用。

loader

因为我的项目是基于Vue全家桶及sass的,所以loader部分会有偏向这方面的介绍,使用TS,React全家桶及less的可以找一下相应的loader。

webpack打包模块时,会把模块拿到module这里的rules中做匹配,如果匹配到,则使用对应的loader进行解析。

因为loader有很多,不好整理,所以我这里就按照项目中不同文件模块所需要的loader来整理说明。

vue

我们一般在基于vue全家桶项目目录中都会使用.vue文件来开发vue组件,所以.vue文件模块需要单独解析。而vue本身就有一个专门解析.vue文件的loader

vue-loader

vue-loader是一个专门解析.vue文件的loader,顾名思义,只是解析.vue文件,并不能解析内部的写法。因为.vue文件一般会有三个部分,一个是template模块,用来写模板语言;一个是script模块,用来写组件的js部分;一个是style模块,用来写组件的css部分。template模块是通过vue的一个plugin插件来解析处理的,script模块跟style模块则提供出来自定义loader进行解析。

所以在loader的配置上也比较简单:

1
2
3
4
5
6
7
8
9
10
{
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
}
]
}
}

javascript

我们的项目中最多的代码就是js代码了,而且现在js的标准已经到了ES2020,很多ES6及以上的标准都需要编译成ES5才能在各种浏览器中使用,所以在webpack打包中,我们也需要解析项目中的js代码。这里就需要使用babel-loader。

babel-loader

讲到babel,我们在webpack中使用的时候,其实有很多复杂的使用方式。这里先写一下在module中的内容:

1
2
3
4
5
6
7
8
9
10
{
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
}

单单只是这样配置是不足以完成js代码的解析的。实际上,如果你的代码中使用了es6及以上标准的语法,还需要配合.babelrc配置文件。这个配置文件的配置内容也是可以直接卸载 rules中,但是因为配置内容比较多,所以最好独立出来。

实际上,这里配置使用的babel-loader在解析代码时是调用了@babel/core,然后由@babel/core来转换代码。转换的过程中会调.babelrc配置文件中的配置。

关于.babelrc文件中的配置介绍,我会另起一篇来做详细介绍。

css

css也是项目中非常重要的一部分。我的项目中使用了sass来开发css。这里就需要安装使用以下loader:

sass-loader

sass-loader是用于解析sass语法的loader,使用这个loader时还需要安装node-sass依赖包。这个包是用于让sass可以在node端进行编译。

安装node-sass也有一些比较麻烦的地方。因为这个包是为了sass能在node端进行编译,所以它的版本要求比较高,不同版本对应不同的node版本范围,需要根据自己的node环境进行选择。同时,安装也不太顺利,一般的依赖包可以直接使用npm进行下载安装,而node-sass直接使用npm下载容易失败,需要考虑更换淘宝源或者使用cnpm安装。

postcss-loader

postcss-loader简单的理解就是一个js工具,用来转化css代码,为css样式处理适配,抹平差异。因为不同浏览器的内核不一样,导致一些样式在各个浏览器中实现不一样。postcss-loader就是用来抹平浏览器之间的差异而出现的。可以让我们在开发时无需考虑这些问题。不过,为css样式加处理实际上不是这个loader本身实现的,而是通过如autoprefixer, postcss-pxtorem配合实现的,所以需要安装并配置。配置上也可以新建一个postcss.config.js。这个后面再单独整理。

css-loader

css-loader主要是用于解释@import和url(),会在import/require()后再解析它们。loader有比较多的配置项,这里因为我的项目中只需要使用要importLoaders这个配置项,所以就只介绍这个,其他的配置项可以在webpack官网上查看。importLoaders是用来指定在css-loader之前使用loader的数量。如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
test: /\.css$/,
use: [
'vue-style-loader',
'style-loader',
{
loader: 'css-loader', // 用于编译@import和url()跟解析css
options: {
// 在 loader 前应用的 loader 的数量,意思是解析css时需要用到'postcss-loader','sass-loader'
// 因为引入的css文件中可能是sass文件
// 如配置1,则应用'postcss-loader',如配置2,则应用'postcss-loader','sass-loader'
importLoaders: 2
}
},
'postcss-loader',
'sass-loader'
]
},

上面配置中,importLoaders设置了2,指定在css-loader之前使用2个loader,而且指定的loader就是postcss-loader跟sass-loader,即指定的loader位于css-loader右侧(下方)最近的数个loader。

style-loader

style-loader的作用是在DOM里插入<style>标签,并将css写入这个标签内。这个loader跟抽离css的插件mini-css-extract-plugin中的内置loader会冲突,所以当使用mini-css-extract-plugin插件时,因为需要用到其loader,所以需要分别使用,比如因为mini-css-extract-plugin插件只能在正式环境使用,需要使用其loader来辅助编译打包时处理抽离,所以正式环境下就不能使用style-loader了。

vue-style-loader

vue-style-loader是配合vue-loader使用的。上面说到,vue-loader是用来解析.vue文件的,但是三个模块是需要分别解析的,而vue-style-loader就是用来解析.vue文件中的style模块的。官方文档介绍到,其实vue-style-loader是从style-loaderfork并基于这个loader开发的,所以作用也跟style-loader一样。但是也有一些区别,增加了对ssr的支持,去掉了一些style-loader支持的功能,所以基于我们项目的需要,还是需要同时使用这两个loader。

css及sass打包配置

基于这些loader,我们就可以进行配置,如sass需要通过scss-loader先做转化为css,然后再用postcss-loader处理加前缀,转单位,然后在使用css-loader处理import/require()语句,处理这些语句是对引入的文件又根据文件类型再使用scss-loader及postcss-loader进行处理。最后再把处理好的css代码通过style-loader,vue-style-loader插入到DOM中。

如果是在正式环境打包,则不使用style-loader,而是使用mini-css-extract-plugin插件配合压缩css代码。

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
{
test: /\.css$/,
use: [
'vue-style-loader', // 用于编译.vue文件中css部分
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 一般使用MiniCssExtractPlugin就不使用style-loader
{
loader: 'css-loader', // 用于编译@import和url()跟解析css
options: {
// 在 loader 前应用的 loader 的数量,意思是解析css时需要用到'postcss-loader','sass-loader'
// 因为引入的css文件中可能是sass文件
// 如配置1,则应用'postcss-loader',如配置2,则应用'postcss-loader','sass-loader'
importLoaders: 2
}
},
'postcss-loader', // 支持增加声明前缀,需要安装autoprefixer,并在postcss.config.js中配置
'sass-loader' // sass编译,还需要安装node-sass依赖。安装node-sass建议使用cnpm
]
},
{
test: /\.scss$/,
use: [
'vue-style-loader',
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
},

其他文件

项目中通常还需要使用各种其他格式的文件,如文字图标会使用到woff,ttf,eot,svg这些格式的文件,图片文件一般有jpg,jpeg,png,gif,svg等格式。一般我们会考虑使用外部链接或者本地文件的形式引入。这里我们主要需要打包处理的是本地文件,所以就需要用到以下这些loader。

file-loader

file-loader就是打包文件时会默认使用文件内容生成MD5哈希值然后使用这个哈希值作为这个文件名字保存到打包目录中,并且不会修改扩展名。file-loader也要一些配置可以使用,这里简单介绍几个:name可以指定打包文件的命名,自定义的命名跟前面打包文件命名类似,可以使用[name](原文件名),[ext](原扩展名),[hash](哈希值),[path](原路径),[N](正则匹配第N个);publicPath指定路径前缀,方便使用CDN;outputPath指定打包目录。

url-loader

url-loader功能类似于file-loader,但是它有一个特点,就是可以根据文件的大小进行不用的打包处理,如小于一个特定的大小,则会打包返回一个DataURL的格式。比如一张图片小于10k,则直接转成DataURL打包存放在调用的文件中,这样会不需要再引用文件了。对于线上使用来说,客户端不需要为这个小文件发多一次请求。当然,这个需要配置limit属性。

其他文件的打包配置

file-loader跟url-loader各有不同,可以根据不同文件使用不同的loader,比如字体文件可以直接使用file-loader,图片类文件可以使用url-loader来分类处理:大小较小的图片可以直接打包进文件中,较大的图片可以打包引入文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
test: /\.(woff|ttf|eot|svg)$/,
use: 'file-loader' // 用于解析通过import/require()引入的文件或外部链接
},
{
test: /\.(jpe?g|png|gif|svg)$/,
use: {
loader: 'url-loader', // 用于把文件编译为base64格式
options: {
name: "image/[contentHash].[ext]", // 超过大小将使用file-loader解析,name为file-loader的配置,contentHash为图片的hash戳,ext是后缀
limit: 100 * 1024, // 尺寸限制
esModule: false // 启用CommonJS模块语法
}
}
}

下一篇整理一下babel跟postcss-loader的配置。

webpack4配置总结(二)

发表于 2020-05-30 | 分类于 JavaScript , webpack

上一篇整理了构建webpack项目的前置准备。根据这些准备,安装依赖,建好目录跟文件,就可以考虑写webpack配置了。

webpack配置的结构

首先,由于要分测试环境跟生产环境,所以webpack配置需要区分开测试跟生产环境的打包配置。但总是有一部分配置是相同的,所以把相同的配置整理在base配置文件中,然后对测试跟生产环境单独建配置文件。这里就需要用到webpack-merge插件。

webpack-merge

webpack-merge插件可以把array合并起来,也可以把多个对象进行合并。这种合并是有深度的,即会对数组的每个元素,对对象的每个属性进行深度合并。合并的原则是遇到数组有相同的元素会保留,遇到对象属性不同会保留后者,遇到函数会先执行函数得到结果再合并。另外会有包括webpack配置中某些值才参数的,会用后者覆盖。

所以webpack-merge非常适合适用于合并webpack配置。我们可以先在base文件中配置一份基础配置内容,然后再引入到测试环境跟生产环境的配置文件中,使用webpack-merge将其合并。也可以考虑把测试环境跟生产环境的配置引入到base文件中,根据环境变量不同来合并配置。

配置方案

这里选择的合并配置方案是把测试环境跟生产环境的配置引入到base文件中,根据环境变量不同来合并配置。这样做法可以在写script命令时把所有打包都指向base配置文件,只需要通过环境变量来区分。当然这个选择因人而异,把base配置引入到测试跟生产环境配置文件中也可以必须要在script命令上写明环境变量。

webpack基础配置

由于确定了配置方案,那么在base文件中,就需要区分不同的打包环境。区分的变量问isDev。

1
let isDev = env.development;

env.development是通过script命令传入的环境变量,可以用来区分打包环境。这个变量将影响我们在一些相同的配置属性上根据不同打包环境做不同的处理。

webpack配置结构

这里先简单的整理一下webpack配置结构。这里介绍的配置结构不是完整的,最好是看webpack官方文档的介绍比较好。

在webpack官网上,官方重点列举了四个核心概念,分别是前面说到的entry,output,还有loader跟plugins。而且base文件中主要也是这四个部分的配置,所以我也会按这四个方面进行整理说明。

entry

entry是用来配置打包入口文件的配置属性。这里可以使用数组或者对象。使用数组说明打包入口有多于一个,这种情况比较适合多页面入口或者打包一个单页面加vendor文件。而使用对象则一般为打包入口只有一个,这种情况比较多为单页面应用。每个入口文件可以配置入口命名,入口路径,前者是用于标识入口,后者是用于执行入口文件的位置。

这里只配置一个入口文件:

1
2
3
4
5
{
entry: {
main: resolve('../src/main.js')
},
}

output

output是用来配置打包的出口的配置属性。这里只能配置一个输出配置,也即只能使用对象。对象中可以有以下属性:

filename: 这个属性是配置打包后文件的命名。可以是String类型,也可以是带动态生成的命名写法(如[name],[chunk],[hash]等这类写法将会在打包后命名文件名是被替换成指定的文件名,chunk值及hash戳)。

chunkFilename: 这个属性是在针对非入口文件,但需要单独打包的文件的文件命名。只要是针对使用require.ensure和import异步加载模块打包后的文件命名。

path: 这个属性是配置打包后文件保存的目录路径。一般我们会指定在根目录的dist目录下。

publicPath: 这个属性是配置最终文件的相对路径的前缀。因为我们一般打包后可以考虑将打包后的文件放到CDN上,在引用时需要要带上相应的CDN路径。配置这一项属性将可以让打包后的文件在引用时自动加上这个前缀。

1
2
3
4
5
6
7
8
{
output: {
filename: isDev ? '[name].js' : '[name].js?v=[chunkhash]',
chunkFilename: isDev ? '[chunkhash].js' : '[id]-chunk.js?v=[chunkhash]',
path: resolve('../dist'),
publicPath: '/'
},
}

loader

loader是webpack打包中非常重要的一项,而是webpack之所以能实现项目打包的核心部分配置。loader就是打包插件,我们使用一系列打包插件,对项目相应的源代码进行转换,它可以处理项目中通过各种方式引入的JavaScript代码,css代码,文件类型文件等等,可以转译es6,TypeScript,scss等等。

配置loader也不难。一般loader需要先安装,然后在webpack配置中module属性里配置,webpack提供了rules这个属性用于配置。因为常见我们需要对多种不同的代码模块进行打包编译转换,所以rules这个属性需要使用一个数组来配置;

1
2
3
4
5
6
7
8
9
10
{
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
}
]
}
}

数组的每一项都是一个对象,都会有基础的两种属性:test是用来匹配对应的模块,一般会匹配一个个文件名(因为我们理解的模块一般是一个文件为一个模块)。匹配文件名可以是一个String类型,或者一个正则等。use是用来指定打包编译转换的loader。如果只使用一个loader,则可以只写loader的名字,如果需要使用多个loader,则需要使用一个数组来列举loader,而数组的每个元素可以是一个String类型,可以是一个JSON格式(有些loader支持配置属性)。数组中loader的执行顺序是从右往左。

这里就先不详细整理一些loader的用法,后面单独整理。

plugins

plugins是用来配置webpack打包过程中使用的插件列表。这些插件可以让你自定义webpack打包过程,比如webpack.DefinePlugin插件可以让你在打包后的代码项目全局上定义一些常量;vue-loader内置了一个插件,是用于配置vue-loader打包编译的。

其他

其他配置项如resolve,devServer,mode和optimization等会在之后的篇章中做说明。

结尾

下一篇将会详细列举一些loader跟plugin的介绍。

webpack4配置总结(一)

发表于 2020-05-30 | 分类于 JavaScript , webpack

最近学习了一下webpack,结合工作中的项目,自己搭建了一套基础的webpack脚手架@listentolife/ll-cli,已经发布到npm上。目前只有一个模板,为基础模板。支持自定义项目初始化信息。

这里也按照这个webpack项目的配置,顺便来做一下学习的总结。

项目配置的目标

整个webpack项目配置完,目标是实现一个基于webpack实现代码打包,基于vue全家桶实现业务开发的项目模板。

项目模板通过脚手架初始化后可以开箱即用,支持本地调试(基于webpack-dev-server),支持打包测试版及正式版(基于webpack-marge)。

基础依赖

这里先把所有涉及的基础依赖,列表列出来。

打包依赖列表

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
"devDependencies": {
"@babel/core": "^7.9.0", // babel核心库
"@babel/plugin-proposal-class-properties": "^7.8.3", // 转化class类
"@babel/plugin-proposal-decorators": "^7.8.3", // 转化装饰器
"@babel/plugin-transform-runtime": "^7.9.0", // 使用babel的helper代码来压缩项目代码
"@babel/preset-env": "^7.9.0", // babel预设,根据开发者的配置按需加载插件
"@babel/runtime": "^7.9.2", // 使用@babel/plugin-transform-runtime需要安装此依赖,包含Babel模块化运行时帮助函数
"autoprefixer": "^9.7.5", // 自动添加css前缀
"babel-loader": "^8.1.0", // babel转译js语法的loader
"clean-webpack-plugin": "^3.0.0", // 打包清理旧打包文件的plugin
"copy-webpack-plugin": "^5.1.1", // 打包时复制文件的plugin
"core-js": "^2.6.11", // js的模块化标准库,babel的底层依赖,目前这个项目使用的是core-js2
"css-loader": "^3.4.2", // 打包css的loader
"file-loader": "^6.0.0", // 打包文件类型的loader
"html-webpack-plugin": "^4.0.1", // 打包html文件的plugin
"html-webpack-tags-plugin": "^2.0.17", // 在html文件中插入js和css文件的plugin
"mini-css-extract-plugin": "^0.9.0", // 将css单独打包成一个文件的插件,它为每个包含css的js文件都创建一个css文件。它支持css和sourceMaps的按需加载。
"node-sass": "^4.13.1", // sass-loader的前置依赖,安装非常麻烦
"optimize-css-assets-webpack-plugin": "^5.0.3", // 手动压缩css的plugin
"postcss-loader": "^3.0.0", // 用js来处理css。负责把CSS解析成抽象语法树结构,在提供给其他插件处理
"postcss-pxtorem": "^5.1.1", // 把px转为rem
"sass-loader": "^8.0.2", // 处理sass的loader
"style-loader": "^1.1.3", // 处理标签中style的loader
"terser-webpack-plugin": "^2.3.5", // 手动压缩js的plugin
"url-loader": "^4.0.0", // 处理url的loader
"vue-loader": "^15.9.1", // 处理vue文件的loader
"vue-style-loader": "^4.1.2", // 处理vue文件中style标签中的css的loader
"vue-template-compiler": "^2.6.11", // 处理vue文件中模板编译
"webpack": "^4.42.0", // 打包工具,webpack本体
"webpack-bundle-analyzer": "^3.6.1", // 打包后分析工具
"webpack-cli": "^3.3.11", // webpack打包命令行工具包
"webpack-dev-server": "^3.10.3", // 开启本地服务器调试的工具
"webpack-merge": "^4.2.2" // 合并webpack配置的工具
}

项目依赖包列表

1
2
3
4
5
6
7
8
9
"dependencies": {
"axios": "^0.19.2", // 处理请求的工具
"eruda": "^2.2.1", // 移动端调试工具
"good-storage": "^1.1.1", // 处理使用storage缓存的工具
"nprogress": "^0.2.0", // 附着在页面顶部的进度条
"vue": "^2.6.11", // vue本体
"vue-router": "^3.1.6", // vue路由
"vuex": "^3.1.3" // vue的状态管理树
},

目录结构

build目录

存放webpack配置。分基础配置文件webpack.base.js,测试环境配置文件webpack.dev.js,生产环境配置文件webpack.prod.js

public目录

存放index.html文件

src目录

存放项目主业务代码:api目录存放请求相关代码;common目录存放js,字体,图片,样式公共文件;components目录存放vue组件;mixins目录存放mixins文件;router目录存放router文件;store存放vuex文件;views目录存放页面级vue组件;App.vue跟main.js是常见的文件,不做说明;

其他

.babelrc:babel配置文件
.browserslistrc:配置浏览器适配
.gitignore:配置git忽略文件或目录
.postcss.config.js:postcss配置文件
package-lock.json,package.json及README.md不做介绍了。

结尾

下一篇开始讲webpack配置。

关于promise题目的打印

发表于 2020-05-05 | 分类于 JavaScript , Promise

原题,请问打印顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
new Promise((resolve, reject) => {
    console.log("1")
    resolve()
}).then(() => {
    console.log("2")
    new Promise((resolve, reject) => {
        console.log("3")
        resolve()
    }).then(() => {
        console.log("4")
    }).then(() => {
        console.log("5")
    })

}).then(() => {
    console.log("6")
})
new Promise((resolve, reject) => {
    console.log("7")
    resolve()
}).then(() => {
    console.log("8")
})

下面是我自己的理解分析过程。

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
new Promise((resolve, reject) => {
    console.log("1") // -> 同步走到7
    resolve() // 存入2的promise
}).then(() => {
    console.log("2") // -> 同步走到3
    new Promise((resolve, reject) => {
        console.log("3") // -> 同步走到9
        resolve() // 存入4的promise,但是同步未走完,要走到9那里
    }).then(() => {
        console.log("4") // -> 同步走完,出栈6
// resolve() // 4先于6存入,所以先执行4,后执行6,然后存入5
    }).then(() => {
        console.log("5") // -> end
// resolve() // 没有then不需要存入
    })
// console.log("9") // 同步走完,出栈8
// resolve() // 存入6。
/**
* 其实实现promise的时候,每个then都是Promise,也就是可以考虑每* 个then执行完都会执行一次resolve,把后面的then入栈
*/

}).then(() => {
    console.log("6") // -> 同步走完,出栈5
// resolve() // 没有then不需要存入
})
new Promise((resolve, reject) => {
    console.log("7") // -> 同步走完,出栈2
    resolve() // 存入8的promise
}).then(() => {
    console.log("8") // -> 同步走完,出栈4
// resolve() // 没有then不需要存入
})

工作总结(12)

发表于 2020-05-05 | 分类于 JavaScript , 工作总结

这篇工作总结将整理一下几个工作中遇到的问题。

webpack插件CleanWebpackPlugin的排除配置

我们一般都知道webpack的CleanWebpackPlugin插件是用在打包时删除之前打包的文件,这样可以帮我们自动清理打包文件。我之所以配置CleanWebpackPlugin插件的排除配置是因为我希望打包完之后dist目录可以保留git仓库的信息,方便我在打包之后直接上传打包文件。但是如果没有做任何配置的话,这个插件是会直接删除整个目录下的文件的,所以需要配置。

(听说原来这个插件没有这个问题,我遇到这个问题是在3.0.0版本,所以不排除是版本的原因)

这个配置我自己找了很久,因为需要排除的不是某几个文件,而且.git目录,但是没有找到有人尝试排除目录,所以一开始只找到配置项cleanOnceBeforeBuildPatterns。这个配置项是配置打包之前删除文件,默认为['**/*']。只要在加上正则匹配非.git目录及目录内文件即可。

1
2
3
4
new CleanWebpackPlugin({
// 必须加这两个目录匹配,前一个是匹配不是.git的目录,后一个是匹配不是.git目录下的文件
cleanOnceBeforeBuildPatterns: ['**/*', '!\.git', '!\.git/**/*']
})

处理图片防盗链的方案

公司的项目有需要抓取文章的内容并需要把图片单独列出来提供使用,但是在这期间遇到一个问题是,因为图片是有防盗链处理的,所以当需要单独列出来的时候图片会报错无法显示。究其原因,是因为请求图片的时候请求的header会自动带上页面的相关信息,服务器就是通过对这部分的判断来选择是否返回图片数据。因此,最好的方案是在当前页面加上下面这个meta属性

1
<meta name="referrer" content="never">

这个mate属性能禁止header发送页面相关信息,可以绕过图片防盗链的检查。

不过这样做也是有缺点的,一是影响页面跳转,比如history.back()会找不到上一个页面,二是一些第三方的统计代码失效。

如果项目是单页面项目(如Vue构建的项目)只是某些页面有处理防盗链图片的话,建议最好是在进入页面时加上这个mate属性,再退出这个页面时再把这个属性置为default。

页面的部分涉及router-view的刷新

一般来说,我们在使用Vue开发项目的时候,一个页面一般会把一些功能模块拆分出来,变成一个个组件,这些组件是被解耦出来的,所以功能也比较独立。但是如果需要根据其中一个组件中数据的变化让另一个组件的数据也随之变化,就会出现一些问题,特别是如果一个组件的数据变化,在router-view中渲染的路由也要刷新。

我遇到的问题就是这种,router-view中渲染的路由需要根据其他组件数据的变化来刷新。

首先,考虑在父组件上通过this.$root.$on来增加一个监听,然后当子组件中数据变化需要让router-view中渲染的路由重新刷新,则可以调用this.$root.$emit来触发监听事件,还可以传入参数。这一步就完成子组件数据变化的监听。

因为需要刷新渲染的路由,所以可以考虑直接在router-view上使用v-if,v-if在切换真假值时,会直接卸载组件(所以这里不能使用v-show),然后当dom渲染完之后再将重新挂载组件,这样就相当于刷新了一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<router-view v-if="isRouterAlive"></router-view>

created() {
console.log("allcontainer created")
this.$root.$on('reload', this.reload);
},
methods: {
reload () {
this.isRouterAlive = false
this.$nextTick(function() {
this.isRouterAlive = true
console.log('reload')
})
}
}

canvas渲染跨域图片的方案

这里有一个需求可能会经常需要做,就是分享的时候生成一张图片,而这张图片一般是通过页面的显示生成的。之前的工作总结中是有整理过的,可以用html2canvas来实现。但是这里有个问题,就是如果dom结构中如果有跨域的图片,则最后生成的图片将不会出现这些跨域的图片。所以这个问题需要处理。

我现在的处理方案是需要后端返回这张图的Blob对象,然后将这个Blob对象转换为本地地址,显示到页面上。转为本地地址的方案可以使用window上自带的方法window.URL.createObjectURL,这个方法传入一个Blob对象,返回一个url,这个url可以在页面上显示图片。这样就可以使跨域图片转为本地图片。

以上就是这次总结的内容。

工作总结(11) - 图片压缩

发表于 2020-05-05 | 分类于 JavaScript , 工作总结

这篇工作总结将整理一下如何实现图片压缩。

之所以需要处理图片压缩,是因为现在很多时候从用户那获得图片并上传,其实对图片的质量要求大部分情况下都不高,而各个客户端的硬件能力也比较强,用户能提供比较高质量的图片,但对要求不高的服务端来说却是一种压力,所以在处理上传图片之前,如果能对图片做一步压缩可以减轻用户的流量负担,也可以减轻服务端的压力。

因为从用户那获得的是一个file对象,所以压缩的对象也是file对象。然后压缩可以通过图片的清晰度,尺寸来压缩。最后,因为图片压缩是一个异步处理,需要有一个回调函数来处理,当然,后面可以改成promise的方式。所以实现图片压缩方法,需要传入的参数就是这四个file,option(包含quality和scale)和callback。

1
2
3
const photoCompress = function (file, options, callback){
// ...
}

首先,file对象需要转成data:URL格式的Base64字符串,这样图片才能被canvas渲染到画布上。这里用到FileReader对象。它能让Web应用程序异步读取存储在用户计算机上的文件,得到几种格式的文件内容。这里用到FileReader.readAsDataURL()来转格式,然后需要调用FileReader.onload在读取操作完成后执行之后的操作

1
2
3
4
5
6
7
8
9
10
11
const photoCompress = function (file, options, callback){
let ready = new FileReader()
/*调用ready.readAsDataURL()来读取指定的Blob对象或File对象中的内容。当读取操作完成时,readyState属性的值会成为DONE,如果设置了onload事件处理程序,则调用之.同时,result属性中将包含一个data:URL格式的字符串以表示所读取文件的内容.*/
ready.readAsDataURL(file)
ready.onload=function(){
// 获取读取后的文件内容
let re = this.result
// 进行下一步的处理
canvasDataURL(re, options, callback)
}
}

获得文件内容之后,就可以来生成一个canvas画布,渲染图片了。先把图片文件加载好,然后在生成canvas标签,宽高直接取图片的宽高,然后生成画布,再把图片渲染到画布上,最后再获得压缩后的图片内容。尺寸的压缩可以在取宽高之后直接使用,得到压缩尺寸后的宽高,而清晰度则在最后获取压缩图片的时候调用canvas.toDataURL()时传入。这一步可以封装一个canvasDataURL()来实现,传入文件内容path,option(包含quality和scale)和callback。

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
const canvasDataURL = function (path, options, callback){
let img = new Image()
img.src = path
img.onload = function () {
let that = this
let imgScale = options.scale && options.scale <= 1 && options.scale > 0 ? options.scale : 1
// 默认按比例压缩
let w = that.width * imgScale
let h = that.height * imgScale
let scale = w / h
w = options.width || w
h = options.height || (w / scale)
let quality = options.quality && options.quality <= 1 && options.quality > 0 ? options.quality : 0.7 // 默认图片质量为0.7
//生成canvas
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
// 创建属性节点
let anw = document.createAttribute("width")
anw.nodeValue = w
let anh = document.createAttribute("height")
anh.nodeValue = h
canvas.setAttributeNode(anw)
canvas.setAttributeNode(anh)
ctx.drawImage(that, 0, 0, w, h)
// quality值越小,所绘制出的图像越模糊
let base64 = canvas.toDataURL('image/jpeg', quality)
// 回调函数返回base64的值
callback(base64)
}
}

这样就可以通过callback获得压缩后的data URI。但是这样的内容还需要转成file对象,才能用做上传或者其他用。

所以,在调用photoCompress方法时传入的callback中,需要做一步转化。首先要先将data URI中的base64编码截出来,转成blob对象,然后在用blob对象转成file对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 将base64编码转成blob对象
const ConvertBase64UrlToBlob = (urlData) => {
// 分离类型跟base64 data
let arr = urlData.split(',')
// 分离出图片类型
let mime = arr[0].match(/:(.*?);/)[1]
let bstr = atob(arr[1])
let n = bstr.length
let u8arr = new Uint8Array(n)
while (n--) {
u8arr[n] = bstr.charCodeAt(n)
}
return new Blob([u8arr], {type: mime})
}

为了外部调用使用时是处理同步状态,所以在调用photoCompress方法时会用一个函数包装,利用promise来处理。

1
2
3
4
5
6
7
8
9
10
11
12
compress (file) {
return new Promise((resolve, reject) => {
photoCompress(file, {
quality: 0.6,
scale: 1
}, (dataUrl) => {
let item = ConvertBase64UrlToBlob(dataUrl);
let newFile = new File([item], file.name, {type: item.type});
resolve(newFile);
})
})
}

这样调用的时候,就可以通过then或者async await来拿到压缩后的file文件。

12…5

Listentolife

Listentolife's Blog

49 日志
19 分类
61 标签
GitHub E-Mail
© 2020 Listentolife
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4
   |