My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

absolute应用

发表于 2019-09-23

问题/背景

table 的 td 内容在内容较多时会自动进行省略处理,然后鼠标滑过弹层显示具体内容

td内容还可以配置在线编辑,鼠标滑过,可以显示小笔图标,点击小笔图标或者td块都可以进行在线编辑然后回车或者鼠标点击输入框之外的地方都可以提交发送给后台

table被包裹在一个div1中,只允许出现水平方向滚动条,不出现垂直方向滚动条

1
2
3
4
.div1{
overflow-x: auto;
overflow-y: hidden;
}

出现的问题就是,如果最后几行的内容过多,弹层显示具体内容时弹层会被遮挡,不能正常飘出来
position1.png

原理知识

父元素设置position:relative,子元素设置position:absolute,配套使用时,子元素才会会在z轴上相对父元素进行xy面的定位
如果最近父元素没有设置position:relative,那么会往父元素的父元素找,看是否设置,如果设置,则相对爷爷元素进行XY面定位,如果没有继续往上找,以此类推
即会相对父元素及以上元素中靠近自己最近的,设置了position:relative的长辈元素进行相对定位

分析

弹层没有按照预期可以弹到div1以外的地方,被困在div1里面,加上div1不能垂直方向滚动,说明弹层没有相对div1定位,
解除div1垂直方向的滚动限制,弹层把div1在垂直方向上撑起来了,滚动滚动条,可以看完整的弹层内容,说明弹层相对div1的子元素定位了,
即从盛装省略内容的标签到div1的标签中,有标签设置了position:relative,

通过查找发现,在线编辑功能的小笔图标使用了position:absolute相对定位,在它最近的div2标签上设置了position:relative,
而div2标签包含了弹层所在标签,属于div1子元素,所以弹层就会相对div2定位,沿Z轴飘在div2这一层上面,就弹不到div1以外了

解决

将div2上position:relative去掉,给在线编辑功能的小笔图标设置position:absolute的标签div3外加div4,设置position:relative

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
解决前:
<div1 style='position:relative'>
<table>
<tbody>
<tr>
<td>
<div2 style='position:relative'>
<div3 style="position:absolute"><i class="fa fa-pencil" ></i></div3>
<popwindow>具体内容</popwindow> /** 组件弹窗div设置有position:absolute**/
</div2>
</td>
</tr>
</tbody>
</table>
</div1>

解决后:
<div1 style='position:relative'>
<table>
<tbody>
<tr>
<td>
<div2>
<div4 style='position:relative'>
<div3 style="position:absolute"><i class="fa fa-pencil" ></i></div3>
</div4>
<popwindow>具体内容</popwindow> /** 组件弹窗div设置有position:absolute**/
</div2>
</td>
</tr>
</tbody>
</table>
</div1>

普通省略内容鼠标滑过
position2.png
在线编辑内容省略时,鼠标滑过
position3.png

My Little World

webpack原理

发表于 2019-07-14

bundle.js能直接运行在浏览器中的原因是:
在输出文件中通过webpack_require函数,定义了一个可以在浏览器中执行的加载函数,来模拟Nodejs中的require语句

webpack_require 函数将所有模块挂载一个对象上,通过判断该对象是否存在该模块实现模块缓存,防止二次加载

原来一个个独立的模块文件被合并到了一个单独的bundle.js的原因是
浏览器不能像Nodejs那样快速的在本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件,
如果模块的数量很多,则加载时间会很长,因此将所有模块都存放在了数组中,执行一次网络加载
输出文件分析

编写loader

常用API

获取loader配置的options

1
2
3
4
5
6
const loaderUtils = require('loader-utils')
module.exports = function(source){
//获取用户为当前loader传入的options
const options = loaderUtils.getOptions(this)
return source
}

返回除了内容之外的东西

this.callback 是 Webpack 给 Loader 注入的 API,以方便 Loader 和 Webpack 之间通信。 this.callback 的详细使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
this.callback( 
// 当无法转换原内容时,给 Webpack 返回一个 Error
err: Error | null,
// 原内容转换后的内容
content: string | Buffer,
// 用于把转换后的内容得出原内容的 Source Map,方便调试
sourceMap?: SourceMap,
// 如果本次转换为原内容生成了 AST 语法树,可以把这个 AST 返回,
// 以方便之后需要 AST 的 Loader 复用该 AST,以避免重复生成 AST,提升性能
abstractSyntaxTree?: AST
);

实例:用 babel-loader 转换 ES6 代码为例,它还需要输出转换后的 ES5 代码对应的 Source Map,以方便调试源码

1
2
3
4
5
6
7
module.exports = function(source) { 
// 通过 this.callback 告诉 Webpack 返回的结果
this.callback(null, source, sourceMaps);
// 当你使用 this.callback 返回内容时,该 Loader 必须返回 undefined,
// 以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return 中
return;
};

Source Map 的生成很耗时,通常在开发环境下才会生成 Source Map,其它环境下不用生成,以加速构建。 为此 Webpack 为 Loader 提供了 this.sourceMap API 去告诉 Loader 当前构建环境下用户是否需要 Source Map。

处理异步流程

1
2
3
4
5
6
7
8
module.exports = function(source) { 
// 告诉 Webpack 本次转换是异步的,Loader 会在 callback 中回调结果
var callback = this.async();
someAsyncOperation(source, function(err, result, sourceMaps, ast) {
// 通过 callback 返回异步执行后的结果
callback(err, result, sourceMaps, ast);
});
};

处理二进制数据

1
2
3
4
5
6
7
8
9
module.exports = function(source) { 
// 在 exports.raw === true 时,Webpack 传给 Loader 的 source 是 Buffer 类型的
source instanceof Buffer === true;
// Loader 返回的类型也可以是 Buffer 类型的
// 在 exports.raw !== true 时,Loader 也可以返回 Buffer 类型的结果
return source;
};
// 通过 exports.raw 属性告诉 Webpack 该 Loader 是否需要二进制数据
module.exports.raw = true; //没有该行 Loader 只能拿到字符串。

缓存加速

在有些情况下,有些转换操作需要大量计算非常耗时,如果每次构建都重新执行重复的转换操作,构建将会变得非常缓慢。 为此,Webpack 会默认缓存所有 Loader 的处理结果,也就是说在需要被处理的文件或者其依赖的文件没有发生变化时, 是不会重新调用对应的 Loader 去执行转换操作的。
如果想让Webpack 不缓存该 Loader 的处理结果,可以这样

1
2
3
4
5
module.exports = function(source) { 
// 关闭该 Loader 的缓存功能
this.cacheable(false);
return source;
};

其他API

this.context:当前处理文件的所在目录,假如当前 Loader 处理的文件是 /src/main.js,则 this.context 就等于 /src。

this.resource:当前处理文件的完整请求路径,包括 querystring,例如 /src/main.js?name=1。

this.resourcePath:当前处理文件的路径,例如 /src/main.js。

this.resourceQuery:当前处理文件的 querystring。

this.target:等于 Webpack 配置中的 Target。

this.loadModule:当 Loader 在处理一个文件时,如果依赖其它文件的处理结果才能得出当前文件的结果时, 就可以通过 this.loadModule(request: string, callback: function(err, source, sourceMap, module)) 去获得 request 对应文件的处理结果。

this.resolve:像 require 语句一样获得指定文件的完整路径,使用方法为 resolve(context: string, request: string, callback: function(err, result: string))。

this.addDependency:给当前处理文件添加其依赖的文件,以便再其依赖的文件发生变化时,会重新调用 Loader 处理该文件。使用方法为 addDependency(file: string)。

this.addContextDependency:和 addDependency 类似,但 addContextDependency 是把整个目录加入到当前正在处理文件的依赖中。使用方法为 addContextDependency(directory: string)。

this.clearDependencies:清除当前正在处理文件的所有依赖,使用方法为 clearDependencies()。

this.emitFile:输出一个文件,使用方法为 emitFile(name: string, content: Buffer|string, sourceMap: {…})。

加载本地loader

方法一 Npm link

Npm link 专门用于开发和调试本地 Npm 模块,能做到在不发布模块的情况下,把本地的一个正在开发的模块的源码链接到项目的 node_modules 目录下,让项目可以直接使用本地的 Npm 模块。 由于是通过软链接的方式实现的,编辑了本地的 Npm 模块代码,在项目中也能使用到编辑后的代码。

完成 Npm link 的步骤如下:

1.确保正在开发的本地 Npm 模块(也就是正在开发的 Loader)的 package.json 已经正确配置好;
2.在本地 Npm 模块根目录下执行 npm link,把本地模块注册到全局;
3.在项目根目录下执行 npm link loader-name,把第2步注册到全局的本地 Npm 模块链接到项目的 node_moduels 下,其中的 loader-name 是指在第1步中的 package.json 文件中配置的模块名称。

链接好 Loader 到项目后就可以像使用一个真正的 Npm 模块一样使用本地的 Loader 了。

方法二 ResolveLoader

ResolveLoader用于配置 Webpack 如何寻找 Loader,默认情况下只会去 node_modules 目录下寻找,为了让 Webpack 加载放在本地项目中的 Loader 需要修改 resolveLoader.modules。
假如本地的 Loader 在项目目录中的 ./loaders/loader-name 中,则需要如下配置:

1
2
3
4
5
6
module.exports = { 
resolveLoader:{
// 去哪些目录下寻找 Loader,有先后顺序之分
modules: ['node_modules','./loaders/'],
}
}

编写plugin

工作原理

plugin内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BasicPlugin{ 
// 在构造函数中获取用户给该插件传入的配置
constructor(options){
}

// Webpack 会调用 BasicPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
compiler.plugin('compilation',function(compilation) {
})
}
}

// 导出 Plugin
module.exports = BasicPlugin;

使用

1
2
3
4
5
6
const BasicPlugin = require('./BasicPlugin.js'); 
module.export = {
plugins:[
new BasicPlugin(options),
]
}

Webpack 启动后,在读取配置的过程中会先执行 new BasicPlugin(options) 初始化一个 BasicPlugin 获得其实例。 在初始化 compiler 对象后,再调用 basicPlugin.apply(compiler) 给插件实例传入 compiler 对象。 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

Compiler 和 Compilation

Compiler 和 Compilation 的含义如下:

1.Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

2.Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

事件流

Webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。
Webpack 通过 Tapable 来组织这条复杂的生产线。 Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 Webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。

Webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。 Compiler 和 Compilation 都继承自 Tapable,可以直接在 Compiler 和 Compilation 对象上广播和监听事件,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/ 
广播出事件
event-name 为事件名称,注意不要和现有的事件重名
params 为附带的参数
/
compiler.apply('event-name',params);

/
监听名称为 event-name 的事件,当 event-name 事件发生时,函数就会被执行。
同时函数中的 params 参数为广播事件时附带的参数。
*/
compiler.plugin('event-name',function(params) {

});

compilation.apply 和 compilation.plugin 使用方法和上面一致。

在开发插件时,还需要注意以下两点:

1.只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
2.传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
3.有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 Webpack,才会进入下一处理流程。例如:

1
2
3
4
5
6
7
compiler.plugin('emit',function(compilation, callback) { 
// 支持处理逻辑

// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});

常用API

监听文件变化

Webpack 会从配置的入口模块出发,依次找出所有的依赖模块,当入口模块或者其依赖的模块发生变化时, 就会触发一次新的 Compilation。
通过【watch-run】 事件可以知道是哪个文件发生变化导致了新的 Compilation

1
2
3
4
5
6
7
8
9
10
/ 当依赖的文件发生变化时会触发 watch-run 事件 
compiler.plugin('watch-run', (watching, callback) => {
// 获取发生变化的文件列表
const changedFiles = watching.compiler.watchFileSystem.watcher.mtimes;
// changedFiles 格式为键值对,键为发生变化的文件路径。
if (changedFiles[filePath] !== undefined) {
// filePath 对应的文件发生了变化
}
callback();
});

默认情况下 Webpack 只会监视入口和其依赖的模块是否发生变化,在有些情况下项目可能需要引入新的文件,例如引入一个 HTML 文件。 由于 JavaScript 文件不会去导入 HTML 文件,Webpack 就不会监听 HTML 文件的变化,编辑 HTML 文件时就不会重新触发新的 Compilation。 为了监听 HTML 文件的变化,我们需要把 HTML 文件加入到依赖列表中,为此可以使用如下代码

1
2
3
4
5
compiler.plugin('after-compile', (compilation, callback) => { 
// 把 HTML 文件添加到文件依赖列表,好让 Webpack 去监听 HTML 模块文件,在 HTML 模版文件发生变化时重新启动一次编译
compilation.fileDependencies.push(filePath);
callback();
});

读取输出资源、代码块、模块及其依赖

在 【emit】 事件发生时,代表源文件的转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容。 插件代码如下:

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
class Plugin { 
apply(compiler) {
compiler.plugin('emit', function (compilation, callback) {
// compilation.chunks 存放所有代码块,是一个数组
compilation.chunks.forEach(function (chunk) {
// chunk 代表一个代码块
// 代码块由多个模块组成,通过 chunk.forEachModule 能读取组成代码块的每个模块
chunk.forEachModule(function (module) {
// module 代表一个模块
// module.fileDependencies 存放当前模块的所有依赖的文件路径,是一个数组
module.fileDependencies.forEach(function (filepath) {
});
});

// Webpack 会根据 Chunk 去生成输出的文件资源,每个 Chunk 都对应一个及其以上的输出文件
// 例如在 Chunk 中包含了 CSS 模块并且使用了 ExtractTextPlugin 时,
// 该 Chunk 就会生成 .js 和 .css 两个文件
chunk.files.forEach(function (filename) {
// compilation.assets 存放当前所有即将输出的资源
// 调用一个输出资源的 source() 方法能获取到输出资源的内容
let source = compilation.assets[filename].source();
});
});

// 这是一个异步事件,要记得调用 callback 通知 Webpack 本次事件监听处理结束。
// 如果忘记了调用 callback,Webpack 将一直卡在这里而不会往后执行。
callback();
})
}
}

修改输出资源

有些场景下插件需要修改、增加、删除输出的资源,要做到这点需要监听 emit 事件,因为发生 emit 事件时所有模块的转换和代码块对应的文件已经生成好, 需要输出的资源即将输出,因此 emit 事件是修改 Webpack 输出资源的最后时机。

所有需要输出的资源会存放在 compilation.assets 中,compilation.assets 是一个键值对,键为需要输出的文件名称,值为文件对应的内容。

设置 compilation.assets 的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
compiler.plugin('emit', (compilation, callback) => { 
// 设置名称为 fileName 的输出资源
compilation.assets[fileName] = {
// 返回文件内容
source: () => {
// fileContent 既可以是代表文本文件的字符串,也可以是代表二进制文件的 Buffer
return fileContent;
},
// 返回文件大小
size: () => {
return Buffer.byteLength(fileContent, 'utf8');
}
};
callback();
});

读取 compilation.assets 的代码如下:

1
2
3
4
5
6
7
8
9
compiler.plugin('emit', (compilation, callback) => { 
// 读取名称为 fileName 的输出资源
const asset = compilation.assets[fileName];
// 获取输出资源的内容
asset.source();
// 获取输出资源的文件大小
asset.size();
callback();
});

判断 Webpack 使用了哪些插件

1
2
3
4
5
6
7
8
// 判断当前配置是否使用了 ExtractTextPlugin, 
// compiler 参数即为 Webpack 在 apply(compiler) 中传入的参数
function hasExtractTextPlugin(compiler) {
// 当前配置所有使用的插件列表
const plugins = compiler.options.plugins;
// 去 plugins 中寻找有没有 ExtractTextPlugin 的实例
return plugins.find(plugin=>plugin.proto.constructor === ExtractTextPlugin) != null;
}

一个例子

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
class EndWebpackPlugin { 

constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}

apply(compiler) {
//done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
compiler.plugin('done', (stats) => {
// 在 done 事件中回调 doneCallback
this.doneCallback(stats);
});
//failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;
compiler.plugin('failed', (err) => {
// 在 failed 事件中回调 failCallback
this.failCallback(err);
});
}
}
// 导出插件
module.exports = EndWebpackPlugin;

//使用
module.exports = {
plugins:[
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数;
new EndWebpackPlugin(() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
}, (err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err); })
]
}

小结

流程概括

Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

1.初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
(启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。)
2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;
3.确定入口:根据配置中的 entry 找出所有的入口文件;
4.编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
5.完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。

流程细节

1.初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。
2.编译:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
3.输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。

初始化阶段事

事件名解释
初始化参数从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。 这个过程中还会执行配置文件中的插件实例化语句 new Plugin()。
实例化 Compiler用上一步得到的参数初始化 Compiler 实例,Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
加载插件依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
environment开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。
entry-option读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。
after-plugins调用完所有内置的和配置的插件的 apply 方法。
after-resolvers根据配置初始化完 resolver,resolver 负责在文件系统中寻找指定路径的文件。

编译阶段事件

事件名解释
build-module使用对应的 Loader 去转换一个模块。
normal-module-loader在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack 后面对代码的分析。
program从配置的入口模块开始,分析其 AST,当遇到 require 等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系
seal所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk

输出阶段事件

事件名解释
should-emit所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。
emit确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。
after-emit文件输出完毕。
done成功完成一次完成的编译和输出流程。
failed如果在编译和输出流程中遇到异常导致 Webpack 退出时,就会直接跳转到本步骤,插件可以在本事件中获取到具体的错误原因。

在输出阶段已经得到了各个模块经过转换后的结果和其依赖关系,并且把相关模块组合在一起形成一个个 Chunk。 在输出阶段会根据 Chunk 的类型,使用对应的模版生成最终要要输出的文件内容

My Little World

webpack优化

发表于 2019-06-29

优化开发体验

目的是提升开发效率

优化构建速度

解决项目庞大时构建的耗时加长的问题

缩小文件查找范围

1.由于Loader对文件转换很耗时,所以应该让尽可能少的文件被处理,适当调整项目目录结构,在配置loader时通过【include】属性缩小处理命中文件范围
2.当安装第三方模块都放在项目根目录的./node_module目录下时,通过指明【resolve.modules】为存放第三方模块的绝对路径,减少寻找第三方模块的递归查找
3.在项目中所有第三方模块都采用main字段去描述入口文件时,只给【resolve.mainFileds】配置main字段,不使用默认值,减少搜索步骤
4.使用【resolve.alias】进行路径映射时,将导入模块的语句,替换成直接使用模块中完整文件的语句,减少耗时的对于lib中文件的解析工作
5.优化【resolve.extension】配置,减少尝试次数
a.后缀列表尽可能小,不要将项目中不可能存在的情况写到后缀列表中
b.频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程
c.在源码中写导入语句时,尽可能带上后缀,从而避免寻找过程
6.合理使用【module.noparse】属性,排除不需要进行模块解析处理的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.
rules:{
//只对项目根目录下src目录中文件采用配置的loader
include:path.resolve(__dirname,'src')
}
2.
resolve:{
//使用绝对路径指明第三方模块存放的位置,减少搜索步骤
modules:[path.resolve(__dirname,'node_module')]
}
3.
resolve:{
//减少入口查找
mainFilds:['main']
}
4.
resolve:{
//减少第三方模块递归解析
alias:{
'react':path.resolve(__dirname,'./node_module/react/dist/react.min.js')
}
}

使用DLLPlugin

一个动态链接库文件可以包含为其他模块调用的函数和数据
提升构建速度的原理:
包含大量复用模块的动态链接库只需被编译一次,在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直接使用动态链接库的代码,
由于动态链接库中大多数包含的是常用的第三方模块,例如react,react-dom,所以只要不升级这些模块的版本,动态连接库就不用重新编译

DllPlugin:打包出一个个单独的动态链接库文件
DllReferencePlugin:用于在主要的配置文件中引入DllPlugin打包好的动态连接库文件

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
//webpack_dll.config.js
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
// JS 执行入口文件
entry: {
// 把 React 相关的放到一个单独的动态链接库
react: ['react', 'react-dom'],
// 把项目需要所有的 polyfill 放到一个单独的动态链接库
polyfill: ['core-js/fn/object/assign', 'core-js/fn/promise', 'whatwg-fetch'],
},
output: {
// 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称,也就是 entry 中配置的 react 和 polyfill
filename: '[name].dll.js',
// 输出的文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
// 存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react
// 之所以在前面加上 _dll_ 是为了防止全局变量冲突
library: '_dll_[name]',
},
plugins: [
// 接入 DllPlugin
new DllPlugin({
// 动态链接库的全局变量名称,需要和 output.library 中保持一致
// 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
// 例如 react.manifest.json 中就有 "name": "_dll_react"
name: '_dll_[name]',
// 描述动态链接库的 manifest.json 文件输出时的文件名称
path: path.join(__dirname, 'dist', '[name].manifest.json'),
}),
],
};

webpack_dll.config.js文件中,DllPlugin中的name参数必须和output

//webpack.config.js
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');

module.exports = {
entry: {
// 定义 入口 Chunk
main: './main.js'
},
output: {
// 输出文件的名称
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// 项目源码使用了 ES6 和 JSX 语法,需要使用 babel-loader 转换
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
plugins: [
// 告诉 Webpack 使用了哪些动态链接库
new DllReferencePlugin({
// 描述 react 动态链接库的文件内容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 动态链接库的文件内容
manifest: require('./dist/polyfill.manifest.json'),
}),
],
devtool: 'source-map'
};

执行构建流程:
先编译出动态链接库相关的文件,执行webpack –config webpack_dll.config.js
在确保动态链接库存在时才能正常编译入口文件,webpack.config.js中的DllReferencePlugin会依赖动态链接库相关文件,执行webpack命令

使用HappyPack

提升构建速度原理:
将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程,从而让webpack在同一时刻处理多个任务
实际表现就是
所有通过Loader处理的文件都先交给happypack/loader去处理,在收集到这些文件的处理权后,HappyPack就可以统一分配了
每通过new HappyPack() 去实例化一个HappyPack,其实就是告诉HappyPack核心调度器如何通过一系列Loader去转换一类文件,
并且可以指定如何为这类转换操作分配子进程
核心调度器的逻辑代码在主进程中,也就是运行着webpack的进程中,核心调度器会将一个个任务分配给当前空闲的子进程,
子进程处理完毕后将结果发送给核心调度,他们之间的数据交换是通过进程间的通信API实现的
核心调度器收到来自子进程处理完毕的结果后,会通知WebPack该文件已处理完毕

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
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HappyPack = require('happypack');

module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: '[name].js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
// 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例
use: ['happypack/loader?id=babel'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: ['happypack/loader?id=css'],
}),
},
]
},
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

在实例化 HappyPack 插件的时候,除了可以传入 id 和 loaders 两个参数外,HappyPack 还支持如下参数:
threads: 代表开启几个子进程去处理这一类型的文件,默认是3个,类型必须是整数。
verbose: 是否允许 HappyPack 输出日志,默认是 true。
threadPool :代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多

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
const HappyPack = require('happypack');
// 构造出共享进程池,进程池中包含5个子进程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });

module.exports = {
plugins: [
new HappyPack({
// 用唯一的标识符 id 来代表当前的 HappyPack 是用来处理一类特定的文件
id: 'babel',
// 如何处理 .js 文件,用法和 Loader 配置中一样
loaders: ['babel-loader?cacheDirectory'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new HappyPack({
id: 'css',
// 如何处理 .css 文件,用法和 Loader 配置中一样
loaders: ['css-loader'],
// 使用共享进程池中的子进程去处理任务
threadPool: happyThreadPool,
}),
new ExtractTextPlugin({
filename: `[name].css`,
}),
],
};

使用ParallelUglifyPlugin

提升构建速度原理:
当webpack有多个JS文件需要输出和压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程,
将对多个文件的压缩工作分配给多个子进程完成,每个子进程其实还是通过UglifyJS去压缩代码,单变成了并行执行

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 path = require(‘path’);
const DefinePlugin = require(‘webpack/lib/DefinePlugin’);
const ParallelUglifyPlugin = require(‘webpack-parallel-uglify-plugin’);

module.exports = {
plugins: [
// 使用 ParallelUglifyPlugin 并行压缩输出的 JS 代码
new ParallelUglifyPlugin({
// 传递给 UglifyJS 的参数
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
},
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 console 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
}
},
}),
],
};

在通过 new ParallelUglifyPlugin() 实例化时,支持以下参数:
test:使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/,也就是默认压缩所有的 .js 文件。
include:使用正则去命中需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
exclude:使用正则去命中不需要被 ParallelUglifyPlugin 压缩的文件。默认为 []。
cacheDir:缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回。cacheDir 用于配置缓存存放的目录路径。默认不会缓存,想开启缓存请设置一个目录路径。
workerCount:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1。
sourceMap:是否输出 Source Map,这会导致压缩过程变慢。
uglifyJS:用于压缩 ES5 代码时的配置,Object 类型,直接透传给 UglifyJS 的参数。

其中的 test、include、exclude 与配置 Loader 时的思想和用法一样
UglifyES 是 UglifyJS 的变种,专门用于压缩 ES6 代码,它们两都出自于同一个项目,并且它们两不能同时使用。
UglifyES 一般用于给比较新的 JavaScript 运行环境压缩代码,例如用于 ReactNative 的代码运行在兼容性较好的 JavaScriptCore 引擎中,为了得到更好的性能和尺寸,采用 UglifyES 压缩效果会更好。
ParallelUglifyPlugin 同时内置了 UglifyJS 和 UglifyES,也就是说 ParallelUglifyPlugin 支持并行压缩 ES6 代码。
如果设置 cacheDir 开启了缓存,在之后的构建中会变的更快。

优化使用体验

通过自动化手段完成一些重复工作,让我们专注于解决问题本身

优化文件监听

通过配置watchOption进行优化
ignored:/node_module/ 不监听node_module下文件 监听更少文件
agregatetionTimeout:值越大,降低构建频率
poll:值越小,降低检查频率
后面两项的配置会使监听模式的反应和灵敏度降低

优化自动刷新性能

1.使用devserver开启inline时,devserver因为不知道某个网页依赖哪几个Chunk,所以会向每个输出的chunk中注入代理客户端代码
当项目输出多个chunk时,就会导致构建缓慢,因此关闭inline进行优化
如果不想以iframe方式去访问,但同时想让网页保持自动刷新功能,则需要手动向网页中注入代理客户端的脚本,
向index.html中注入webpack-dev-server.js,但要注意发布到线上时要删掉这段用于开发的代码

2.开启热替换时,控制台打印信息不能标明模块信息,可以使用NameModulesPlugin插件解决,从而在控制台打印出被修改的模块名称
注意关闭默认inline模式并手动注入客户端的方法,不能用于模块热替换的情况,原因在于模块热替换的运行依赖每个chunk中都包含代理客户端的代码

优化输出质量

目的是为用户呈现体验更好的网页

减少用户能够感知到的加载时间(首屏时间)

区分环境

问题:
开发过程会涉及到调试的代码,没必要发到线上给到用户
开发环境和线上接口地址不同,使用不同环境数据
线上代码会进行压缩,开发代码不需要、

解决:
当代码中使用了process模块的语句时,webpack会自动打包加入process模块代码来支持非nodejs运行环境
当代码中没有使用时,就不会打包加入
可以在源码中使用process.env.NODE_ENV环境变量去判断执行开发/线上环境的代码
环境变量的设置通过DefinePlugin设置
设置后,环境变量的值在webpack处理过程中会被代入源码中,替换掉process.env.NODE_ENV
访问proces的语句被替换,webpack也就不会在打包时加入process模块了

注意:DefinedPlugin定义的环境变量只对Webpack需要处理的代码有效,而不会影响Nodejs运行时的环境变量

1
2
3
4
5
6
7
8
9
10
11
12
const DefinePlugin = require('webpack/lib/DefinePlugin');

module.exports = {
plugins: [
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production
'process.env': {
NODE_ENV: JSON.stringify('production')//环境变量的值需要一个由双引号包裹的字符串'"production"'
}
}),
],
};

压缩代码

提升网页加载速度,减少网络传输流量,混淆代码以防有人下载代码进行代码分析和改造
通过插件形式引入UglifyJs,利用UglifyJs分析JS代码语法树,理解代码含义,
从而去掉无效代码,去掉日志输出代码,缩短变量名等优化,仅用于es5
UglifyJsPlugin:封装UglifyJs实现压缩
ParallelUglifyPlugin:多进行并行处理压缩
uglifyjsWebpackPlugin:引入UglifyES,压缩es6,要去掉.babelrc文件中的babel-preset-env,否则会将es6转es5
css-loader?minimize:压缩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
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
1.UglifyJsPlugin
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');//使用内置插件
module.exports = {
plugins: [
new UglifyJSPlugin({
compress: {
// 在 UglifyES 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出,默认会保留空格
beautify: false,
// 删除所有的注释
comments: false,
}
}),
],
};

2.uglifyjs-webpack-plugin
const UglifyESPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyESPlugin({
uglifyOptions: {//相比UglifyJSPlugin 多了一层
compress: {
// 在 UglifyES 删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句,可以兼容ie浏览器
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
output: {
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
}
}
}),
],
};

3.压缩CSS
const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
module: {
rules: [
{
test: /\.css/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'] // 压缩 CSS 代码
}),
},
]
},
plugins: [
// 用 WebPlugin 生成对应的 HTML 文件
new WebPlugin({
template: './template.html', // HTML 模版文件所在的文件路径
filename: 'index.html' // 输出的 HTML 的文件名称
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
}),
],
};

使用CDN加速

webpack接入CDN需要满足:
1.静态资源的导入URL需要变成指向CDN服务的绝对路径URL,而不是相对于HTML文件的URL
2.静态资源的文件名需要带上由文件内容算出来的Hash值,以防止被缓存
3.将不同类型资源放到不同域名CDN服务上,以防止资源的并行加载阻塞
实现主要依赖publicPath设置存放静态资源的CDN目录URL

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
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const {WebPlugin} = require('web-webpack-plugin');

module.exports = {
entry: {
// Chunk app 的 JS 执行入口文件
app: './main.js'
},
output: {
// 给输出的 JavaScript 文件名称加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的线上目录
publicPath: '//js.cdn.com/id/',
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
// 增加对 CSS 文件的支持
test: /\.css/,
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
// 压缩 CSS 代码
use: ['css-loader?minimize'],
// 指定存放 CSS 中导入的资源(例如图片)的线上目录
publicPath: '//img.cdn.com/id/'
}),
},
{
// 增加对 PNG 文件的支持
test: /\.png/,
// 给输出的 PNG 文件名称加上 Hash 值
use: ['file-loader?name=[name]_[hash:8].[ext]'],
},
]
},
plugins: [
// 使用 WebPlugin 自动生成 HTML,会根据publicPath用线上地址替换原来相对地址
new WebPlugin({
// HTML 模版文件所在的文件路径
template: './template.html',
// 输出的 HTML 的文件名称
filename: 'index.html',
// 指定存放 CSS 文件的线上目录
stylePublicPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 给输出的 CSS 文件名称加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 压缩输出的 JS 代码
],
};

使用tree Shaking剔除用不上的代码

Tree Shaking可以分析出那些代码被用上了,哪些没有
Tree Shaking正常工作的前提是,提交给webpack的js代码必须采用了ES6的模块化语法,因为es6模块化语法是静态的
(在导入,导出语句中的路径必须是静态字符串,而且不能放入其他代码块中),这让webpack可以简单地分析出那些export被import了。
实现:
1.修改.babelrc文件,保留ES6语法

1
2
3
4
5
6
7
8
9
10
{
"presets":[
[
"env",
{
"module":false//关闭模块转换功能,保留ES6语法
}
]
]
}

可以使用webpack –display-used-exports查看分析结果
2.使用uglifyJS剔除用不上的代码
可以在直接在配置文件中配置
也可以使用命令行执行
webpack –display-used-exports –optimize-minimize

处理但第三方库时,利用mainField告诉webpack采用那份入口文件,
当指定入口于文件采用es6模块化语法时,从而可以使用treeShaking进行代码优化

提取公共代码

问题:
相同资源被重复加载,浪费用户的流量和服务器成本
每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验

解决:
将多个页面公共代码部分抽离成单独文件,用户在第一次访问后,公共文件代码被浏览器缓存
在用户切换其他页面时,则不会再重新加载存放公共代码的文件,而是直接从缓存中获取
从而
减少网络传输流量,降低服务器成本
虽然用户第一次打开网站的速度得不到优化,但之后访问其他页面的速度将提高

实现:
–根据网站所使用的技术栈,找出网站所有页面都需要用到的基础库(第三方库),将他们提取到一个单独的文件base.js中
该文件包含了所有网页的基础运行环境,用于长期缓存,提高响应速度

–剔除了各个页面中被base.js包含的部分代码后,再找出所有页面都依赖的公共部分的代码,将他们提取到commom.js

–再为每个网页都生成一个单独的文件,不包含base.js和common.js中包含的部分,只包含各页面单独需要的部分代码

– 依赖CommonsChunkPlugin插件
CommonsChunkPlugin实例会生成一个新的Chunk,这个chunk中包含了被提取的代码
通过name属性告诉插件新生成的chunk的名称,chunks属性指明从哪些已有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
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
const path = require('path');
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');

// 使用 AutoWebPlugin,自动寻找 pages 目录下的所有目录,把每一个目录看成一个单页应用
const autoWebPlugin = new AutoWebPlugin('pages', {
template: './template.html', // HTML 模版文件所在的文件路径
// 提取出所有页面公共的代码
commonsChunk: {
name: 'common',// 提取出公共代码 Chunk 的名称
},
});

module.exports = {
// AutoWebPlugin 会找为寻找到的所有单页应用,生成对应的入口配置,
// autoWebPlugin.entry 方法可以获取到生成入口配置
entry: autoWebPlugin.entry({
// 这里可以加入你额外需要的 Chunk 入口
base: './base.js'
}),
output: {
filename: '[name]_[chunkhash:8].js',// 给输出的文件名称加上 hash 值
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
// 排除 node_modules 目录下的文件,node_modules 目录下的文件都是采用的 ES5 语法,没必要再通过 Babel 去转换
exclude: path.resolve(__dirname, 'node_modules'),
},
{
test: /\.css/,// 增加对 CSS 文件的支持
// 提取出 Chunk 中的 CSS 代码到单独的文件中
use: ExtractTextPlugin.extract({
use: ['css-loader?minimize'] // 压缩 CSS 代码
}),
},
]
},
plugins: [
autoWebPlugin,
// 为了从 common 中提取出 base 也包含的部分,减小common体积
new CommonsChunkPlugin({
// 从 common 和 base 两个现成的 Chunk 中提取公共的部分
chunks: ['common', 'base'],
// 把公共的部分放到 base 中
name: 'base'
}),
new ExtractTextPlugin({
filename: `[name]_[contenthash:8].css`,// 给输出的 CSS 文件名称加上 hash 值
}),
new DefinePlugin({
// 定义 NODE_ENV 环境变量为 production 去除 react 代码中的开发时才需要的部分
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
// 压缩输出的 JS 代码
],
};

common.js没有内容的时候解决
1.CommonsChunkPlugin的minChunks属性表示文件要被提取出来时需要在指定的Chunks中出现的最小次数,值越小,被提到common中的文件越多
2.根据各个页面之间的相关性选取其中的部分页面时,可用CommonChunkPlugin提取这部分被选出的页面的公共部分,
而不是提取所有页面的公共部分,而且这样的操作可以叠加多次,缺点是配置复杂,需要根据页面之间关系去思考如何配置
但该方法不通用

分割代码按需加载

问题
单页面应用一次性加载所有功能代码,实际在每个阶段只可能使用其中一部分

解决
将整个网站划分成一个个小功能,再按照每个功能的相关程度将他们分成几类
将每一类合并成一个Chunk,按需加载对应的Chunk
不要按需加载用户首次打开网站时需要看到的画面所对应的功能,将其放到执行入口所在的chunk中,
以减少用户能感知的网页加载时间
对于不依赖大量代码的功能点,可对其进行按需加载

实现

1
2
3
4
5
6
7
//oneName.js
module.exports= function(){}

//加载语句
import(/* webpackChunkName:"oneName" */ './oneName').then((oneName)=>{
oneName()
})

webpack内置了对import(*)语句的支持,当webpack遇到类似的语句时会这样处理
以oneName.js为入口重新生成一个Chunk
当代码执行到import所在的语句时才去加载由Chunk对应生成的文件
import返回一个Promise,当文件加载成功时可以在Promise的then方法中获取oneName.js导出的内容

/* webpackChunkName:”oneName” */ 含义是为动态生成的Chunk赋予一个名称,以方便我们追踪和调试代码
如果不指定,则其默认的名称将会是[id].js
同样需要在webpack中配置chunkFilename属性,来指定动态生成的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
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
const path = require('path');

module.exports = {
// JS 执行入口文件
entry: {
main: './main.js',
},
output: {
// 为从 entry 中配置生成的 Chunk 配置输出文件的名称
filename: '[name].js',
// 为动态加载的 Chunk 配置输出文件的名称
chunkFilename: '[name].js',
path: path.resolve(__dirname, './dist'),
},
module: {
rules: [
{
test: /\.js$/,
use: ['babel-loader'],
exclude: path.resolve(__dirname, 'node_modules'),
},
]
},
devtool: 'source-map' // 输出 source-map 方便直接调试 ES6 源码
};

//在.babellrc中需要引入组件来识别import(*)语法
{
"presets":[
"env",
"react"
],
"plugins":[
"syntax-dynamic-import"
]
}
//实际应用
import React, {PureComponent, createElement} from 'react';
import {render} from 'react-dom';
import {HashRouter, Route, Link} from 'react-router-dom';
import PageHome from './pages/home';

/**
* 异步加载组件
* @param load 组件加载函数,load 函数会返回一个 Promise,在文件加载完成时 resolve
* @returns {AsyncComponent} 返回一个高阶组件用于封装需要异步加载的组件
*/
function getAsyncComponent(load) {
return class AsyncComponent extends PureComponent {

componentDidMount() {
// 在高阶组件 DidMount 时才去执行网络加载步骤
load().then(({default: component}) => {
// 代码加载成功,获取到了代码导出的值,调用 setState 通知高阶组件重新渲染子组件
this.setState({
component,
})
});
}

render() {
const {component} = this.state || {};
// component 是 React.Component 类型,需要通过 React.createElement 生产一个组件实例
return component ? createElement(component) : null;
}
}
}

// 根组件
function App() {
return (
<HashRouter>
<div>
<nav>
<Link to='/'>Home</Link> | <Link to='/about'>About</Link> | <Link to='/login'>Login</Link>
</nav>
<hr/>
<Route exact path='/' component={PageHome}/>
<Route path='/about' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-about' */'./pages/about')
)}
/>
<Route path='/login' component={getAsyncComponent(
// 异步加载函数,异步地加载 PageAbout 组件
() => import(/* webpackChunkName: 'page-login' */'./pages/login')
)}
/>
</div>
</HashRouter>
)
}

// 渲染根组件
render(<App/>, window.document.getElementById('app'));

提升流畅度

提升代码性能
1.使用prepack优化代码运行时效率
通过在编译阶段预先执行源码来得到执行结果,再直接将运行结果放到编译后的代码中,而不是在代码运行时才去求值

prepack工作原理和流程大致如下:
通过Babel将JS源码解析成抽象语法树(AST),以更细粒度地分析源码
PrePack实现了一个JS解释器,用于执行源码,借助这个解释器,Prepack才能理解源码具体是如何执行的
并将执行过程中的结果返回到输出中

因为还处于开发阶段,不能识别DOM api和部分nodejs API 代码,优化后文件尺寸可能大大增加,性能可能更差所以目前还不适合用于处理线上代码

2.开启Scope Hoisting
分析模块之间的依赖关系,尽可能将被打散的模块合并到一个函数中,大前提是不能造成代码冗余,
因此只有那些被引用了一次的模块才能被合并
由于Scope Hosting需要分析模块之间的依赖关系,因此源码必须采用ES6语句,不然它将无法生效
对于非ES6模块化语法的代码,webpack会降级处理且不使用Scope Hoisting优化。
为了知道webpack对哪些代码做了降级处理,可以在启动webpack时带上–display-optimization-bailout参数
这样输出的日志就会告知是什么原因导致了降级处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const path = require('path');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');

module.exports = {
// JS 执行入口文件
entry: './main.js',
output: {
// 把所有依赖的模块合并输出到一个 bundle.js 文件
filename: 'bundle.js',
// 输出文件都放到 dist 目录下
path: path.resolve(__dirname, './dist'),
},
resolve: {
// 针对 Npm 中的第三方模块优先采用 jsnext:main 中指向的 ES6 模块化语法的文件
mainFields: ['jsnext:main', 'browser', 'main']
},
plugins: [
// 开启 Scope Hoisting
new ModuleConcatenationPlugin(),
],
};

好处:
代码体积更小,因为函数申明语句会产生大量代码
代码在运行时因为创建的函数作用域变少,所以内存开销也变小了

输出分析

执行命令
webpack –profile –json >stats.json
可以将构建相关的信息输出到stats.json文件中
1.打开http:\//webpack.github.io/analyse/ 上传stats文件,使用官方Webpack Analyse分析输出结果
2.使用webpack-bundle-analyzer进行分析
安装webpack-bundle-analyzer到全局
在项目根目录中执行webpack-bundle-analyzer,浏览器会打开对应网页并展现打包结果

侧重优化输出质量的配置
侧重优化开发体验的配置

My Little World

Vue编译初探

发表于 2019-06-09

Vue声明周期
初始化之后调用$mount会挂载组件编译template

编译

compile编译可以分成parse,optimize与generate三个阶段,最终得到render function
parse
parse 会用正则等方式循环切割字符串,解析template模板中的指令,class,style等数据,形成AST
optimize
标记static静态结点,这是编译过程的一处优化
generate
将AST转化成render function字符串的过程,得到的结果是render的字符串以及staticRenderFns

当render function 被调用的时候,因为会读取所需对象的值,所以会触发getter函数进行【依赖收集】
【依赖收集】的目的是将观察者Watcher对象存放到当前闭包中的订阅者Dep的subs中
在修改对象的值的时候,会触发对应的setter,setter通知之前【依赖收集】得到的Dep中的每一个Watcher
告诉他们自己的值改变了,需要重新渲染视图

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
Vue.prototype.$mount = function(){
//挂在组件 已生成渲染函数,被调用
return mountComponent(this,el,hydrating)
}

//缓存 mount
var mount = Vue.prototype.$mount;

//重新覆盖 编译的功能
Vue,prototype.$mount = function(){
//挂在组件 生成render function
return mount.call(this,el)
}
编译过程:字符串模板转成渲染函数
运行时:调用渲染函数
独立构建 = 编译+运行时
运行时构建 = 运行时

//模板的编译 分成parse,optimize与generate三个阶段,最终得到render function
compileToFunctions(template)
//render function生成通过new Function,可以将函数功能通过字符串传递进去生成新函数
var render = new Function(参数,函数主体内容字符串)
其中函数主体字符串的功能就是返回parse生成的抽象语法树AST

渲染函数在哪被调用
mountComponen(){
//模板编译完成,实例挂载之前调用生命周期函数
callHook(vm,'beforeMount')

初始化 updateComponent函数
在非生产环境下config.performance为true,
初始化 updateComponent时有进行性能追踪的相关代码
(进行性能追踪4个场景:
组件【初始化】时
【编译】时模板转渲染函数时
通过【渲染】函数生成虚拟DOM时
【打补丁】,虚拟DOM转真实Dom时)
vm.update(vm._render(),hydrating)
vm._render() === vm.$options.render() //生成虚拟节点vnode
vm.update() 把vm._render()生成的虚拟节点渲染成真实的DOM
updateComponent 用作参数生成Watcher,即数据发生变化时,可以重新渲染DOM节点

}

vm.$option.render 渲染函数生成

1.生成
vm.$option.render = render 构造器函数调用parse,generate生成函数主体字符串,
再调用new Function返回函数
2.探究渲染函数this指向
initProxy
【渲染函数的作用域代理】
Proxy在目标对象之前架设一层拦截,拦截啥?
读取get;设置set;key in proxyObject 属性检测;with(){}
对一个new Proxy生成的对象进行上述操作时就会引发has钩子函数
vnode = render.call(vm._renderProxy,vm.$creatElement)

1
2
3
4
5
(function anonymous(){//一个渲染函数
//vm.renderProxy 访问变量A就会进行拦截
//调用proxy的has钩子函数 ,钩子函数中进行依赖收集,与生成的watcher进行绑定
with(this){return ...变量A...}
})

vue 对象生成

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
//vue 对象生成
(function(global,factory){
typeof exports === 'object' && typeof module !=='undefinded' ?module.export = factory():
typeof define === 'function' && define.amd ? define(factory):
(global.Vue = factory());//window.Vue cmd AMD Comonjs
})(this,function(){
var ASSET_TYPES = [
'components',
'directives',
'filters'
]
//全局配置对象 挂载在Vue对象上,用作接口,进行自定义策略
(怎样处理自定义挂载在vue上的属性,和vue自身的同名属性,二者合并options时的处理方案)
//Vue.config.optionMergeStrategies.xxxx = function(){}
var config = {
optionMergeStrategies:Object.create(null)
}
//自定义策略
var strats = config.optionMergeStrategies
strats.data = function(parentVal,childVal,vm,key){
return function mergedInstanceDataFn(){

}
}
//默认策略
function defaultStrats(parentVal,childVal,vm){
return childVal === undefined? parentVal:childVal
}
var has = function(obj,key){
return obj != null && Object.hasOwnProperty.call(obj,key)
}
function mergeOption(parent,child,vm){
var options = {}
var key
for(key in parent){ //parent=>component directive filters 本身具备的
mergeFild(key)
}
for(key in child){ //child =>el data component 外部传参进来
if(!has(parent,key)){ //拦截重复操作
mergeFild(key)
}
}
//选项的处理celve
function mergeFild(key){
//生成最终需要数据
//合并策略 自定义策略 默认策略
console.log(key)
var strat = strats[key] || defaultStrats
options[key] = strat(parent[key],child[key],vm)
}

return options
}
function initMixin(vue){

Vue.prototype._init = function(options){
var vm = this
//合并选项 VUE.option option
vm.$options = mergeOption(Vue.options,options||{},vm)
}
}
function initGlobalAPI(Vue){
var configDef = {};
configDef.get = function(){
return config
}
configDef.set = function(val){
console.error('不要修改config')
}
//Vue.config = config 对修改config做拦截,但可以对config的属性进行操作
//Vue.config.optionMergeStrategies 可以进行扩展自定义策略
Object.defineProperty(Vue,'config',configDef)
}

function Vue(options){
if(!(this instanceof Vue)){
console.error('....')
}
this._init(options)
}
Vue.options = Object.create(null);
ASSET_TYPES.foreach(function(type){
Vue.options[type] = Object.create(null)
})
initMixin(Vue)
initGlobalAPI(Vue)initGlobalAPI(Vue)
return Vue
})
My Little World

react-navigation

发表于 2019-06-02

react-navigation

安装

1
2
3
yarn add react-navigation
yarn add react-native-gesture-handler
react-native link react-native-gesture-handler //Link 所有的原生依赖

配置

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
APP.js
import React from "react";
import { View, Text } from "react-native";
import { createStackNavigator, createAppContainer } from "react-navigation";
class HomeScreen extends React.Component {
render() {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<Text>Home Screen</Text>
</View> );
} }
const AppNavigator = createStackNavigator(
{
HomeComponent: { screen: HomeScreenComponent } //screen属性设置组件路由,HomeScreen只是一个组件
Home: HomeScreen,//直接配置页面路由
Details: DetailsScreen
},
{
initialRouteName: "Home"//指定默认路由
defaultNavigationOptions: { //指定全局默认标题样式
headerStyle: {
backgroundColor: '#f4511e',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: 'bold',
},
},
});
const AppContainer = createAppContainer(AppNavigator);
export default class App extends React.Component {
someEvent() {
// call navigate for AppNavigator here:
this.navigator &&
this.navigator.dispatch( //在 App 容器中使用 dispatch,可以使用 ref 来调用 dispatch 方法
NavigationActions.navigate({ routeName: someRouteName })
);
}
render() {
return <AppContainer
//每当导航器管理的 navigation state 发生变化时,都会调用该函数
onNavigationStateChange={handleNavigationChange}
uriPrefix="/app"//应用可能会处理的 URI 前缀, 在处理一用于提取传递给 route 的一个 深度链接时将会用到
ref={nav => {
this.navigator = nav;
}}
/>;
}
}

跳转

this.props.navigation.navigate(路由名,传参对象)//跳转其他页,如果跳转本页,没反应
this.props.navigation.push(路由名)//可以重新加载当前页
每次调用 ‘ push ‘ 时, 我们会向导航堆栈中添加新路由。
当你调用 ‘ navigate ‘ 时, 它首先尝试查找具有该名称的现有路由, 并且只有在堆栈上没有一个新路由时才会推送该路由。
读取页面组件中的参数的方法:
this.props.navigation.state.params
this.props.navigation.getParam(key值)

如果当前页面可以执行返回操作,则 stack navigator 会自动提供一个包含返回按钮的标题栏
如果导航堆栈中只有一个页面,则没有任何可返回的内容,因此也不存在返回键。
this.props.navigation.goBack() //手动返回上一页

如果处在堆栈深处,上面有多个页面,此时想要将上面所有的页面都销毁,并返回第一个页面。
在这种情况下,我们知道我们要回到’ Home ‘,所以我们可以使用’ navigate(‘Home’) ‘,而不是push
另一个选择是’ navigation.popToTop() ‘,它可以返回到堆栈中的第一个页面。

如何发现用户离开和回来的某页面?
一个包含 页面 A 和 B 的 StackNavigator ,当跳转到 A 时,componentDidMount 方法会被调用; 当跳转到 B 时,componentDidMount 方法也会被调用,但是 A 依然在堆栈中保持 被加载状态,他的 componentWillUnMount 也不会被调用。
当从 B 跳转到 A,B的 componentWillUnmount 方法会被调用,但是 A 的 componentDidMount方法不会被调用,应为此时 A 依然是被加载状态。
React Navigation 将事件发送到订阅了它们的页面组件: 有4个不同的事件可供订阅:willFocus、willBlur、didFocus 和 didBlur。

标题栏

使用 组件静态属性navigationOptions

内容配置

title属性
方式一:对象

1
2
3
static navigationOptions = {
title: 'Home',
};

方式二:返回对象的属性

1
2
3
4
5
static navigationOptions = ({ navigation }) => {
return {
title: navigation.getParam('otherParam', 'A Nested Details Screen'),
};
};

尝试在navigationOptions中使用this.props可能很诱人,但因为它是组件的静态属性,所以this不会指向一个组件的实例,因此没有 props 可用。 相反,如果我们将navigationOptions作为一个函数,那么React Navigation将会用包含{navigation,navigationOptions,screenProps}的对象调用它 – 在这种情况下,我们只用关心navigation,它是与传递给页面的this.props.navigation相同的对象

从已加载的页面组件本身更新当前页面的navigationOptions配置
this.props.navigation.setParams({otherParam: ‘Updated!’})

标题样式

headerStyle:一个应用于 header 的最外层 View 的 样式对象, 如果设置 backgroundColor ,他就是header 的颜色。对象
headerTintColor:返回按钮和标题都使用这个属性作为它们的颜色。字符串
headerTitleStyle:如果想为标题定制fontFamily,fontWeight和其他Text样式属性,我们可以用它来完成。对象

全局配置

配置路由时利用defaultNavigationOptions配置
当全局设置了默认样式,具体页面也设置了static navigationOptions,则优先使用具体页面

使用组件

headerTitle:配置一个组件,自定义标题的样式
headerRight:自定义右侧按钮
headerLeft:自定义左侧/返回按钮
headerBackImage:自定义返回按钮图片
注意在navigationOptions中this绑定的不是 HomeScreen 实例,所以你不能调用setState方法和其上的任何实例方法。 这一点非常重要,因为标题栏中的按钮与标题栏所属的页面进行交互是非常常见的。

My Little World

二级菜单实现

发表于 2019-04-12

背景

page A是一个管理页面,数据通过table 展示
page B是一个详情页面,通过page A 中的一条数据跳转链接进来
page B数据获取时的参数需要在page A跳转时带进来
page B 本来只是一个简单页面,现在要在page B中添加一列菜单,可以导航多个不同内容详情
即现在page A要跳转的是一个带菜单的页面,点击每个菜单跳转不同的页面

原理

1.vue-router配置时,children属性配置的子页面,在访问时,会在父页面的router-view标签部分进行填充
如果父页面没有router-view标签子页面内容就没地方展示
菜单就放在父页面,点击跳转子页面路由
2.因为页面跳转完成后,当前路由对象不会携带自己的children属性,给子路由配置title,父页面拿不到
所以父页面的菜单映射关系需要在其他地方配置,传递进来

解决

方案一

子菜单名和对应路由的映射,放在page A跳转配置中,page A 配置的点击跳转的页面是其中一个子路由

1
2
3
4
5
6
7
{title: '名称', value: 'name', display: true,
shadow: {path: 'Adetail_C', key: '',title:'AAAA',submenu:[{
path:'Adetail_C',name:'C'
},{
path:'Adetail_D',name:'D'
}]},
},

新增子菜单路由页面page A-detail-router,子菜单映射页面page C和page D

页面page A-detail-router中,可以通过this.$route对象拿到page A配置的菜单映射关系,从而渲染菜单
切换子页面时,需要增加把参数传递到子页面中的逻辑

page C和page D根据路由中传递过来的page A点击数据的信息进行数据请求和页面渲染

在项目路由配置文件中配置路由

1
2
3
4
5
6
7
8
{ path: 'A',  name: 'A', title: 'XX管理', meta: {keepAlive: true}, component: () => import('@/pageA') },
{ path: 'AdetailRouter', name: 'AdetailRouter', title: 'XX详情', meta: {keepAlive: true}, component: () => import('@/A-detail-router'),
children:[{
path:'/Adetail_C',name: 'Adetail_C',meta: {previousMenu: 'A'}, component: () => import('@/Adetail_C')
},{
path:'/Adetail_D',name: 'Adetail_D',meta: {previousMenu: 'A'},component: () => import('@/Adetail_D')
}]
},

children配置path时,以‘/’开头和不以‘/’开头区别:
以’/‘开头最终访问路径就是配置路径‘#/子path’,相当于子路由当一级路由
但不以‘/’开头最终的访问路径是‘#/父path/子path’,
这里使用‘/’开头是因为更外层菜单,在跳转page A同级路由时使用的是push({path:xxx})
如果使用不以‘/’开头的配置,跳转page A同级路由,路径就会变成‘#/父path/xxx’,即父路径下跳转
会因为没有相应配置路由找不到页面,所以使用‘/’开头,当作父路由跳转
其实在跳转page A同级路由时使用的是push({name:xxx}),就可以直接解决‘#/父path/xxx’问题,
跳转正确页面

缺点:
直接通过浏览器刷新子页面时,路由对象的params属性数据清空,
路由页面菜单映射没有数据支持,菜单没有内容
子页面路由因此也拿不到参数,无法请求数据
动作流向只有利用路由对象的meta属性的previousMenu跳转回page A

方案二

子菜单名和对应路由的映射放在单独json文件中,利用子菜单名字获取映射关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//submenu.json
{
Adetail:{
title:'AAAA'
submenu:[{
name:'Adetail_C',//路由里配置的name
label:'C',
show:true //之后根据路由参数query决定是否显示该菜单
},
{
name:'Adetail_ D',
label:'D',
show:true
}]
}
}

在路由页面page A-detail-router中根据访问的子菜单路由名获取二级菜单,这样解决刷新页面菜单数据无处获取的问题

使用动态路由传递参数id保证页面刷新时子页面params至少有一个属性可以依赖来获取数据
更改路由配置

1
2
3
4
5
6
7
8
{ path: 'A',  name: 'A', title: 'XX管理', meta: {keepAlive: true}, component: () => import('@/pageA') },
{ path: 'Adetail', name: 'Adetail', title: 'XX详情', meta: {keepAlive: true}, component: () => import('@/A-detail-router'),
children:[{
path:'C/:id',name: 'Adetail_C',meta: {previousMenu: 'A'}, component: () => import('@/Adetail_C')
},{
path:'D/:id',name: 'Adetail_D',meta: {previousMenu: 'A'},component: () => import('@/Adetail_D')
}]
},

‘:id’对应路由对象中params属性的id属性,所以访问时,一定要在跳转的路由中配置{params:{id:xxxxxx}}
更改路由页面page A-detail-router中跳转其他路由页面时,参数配置

1
2
3
4
5
6
7
8
gotoPage (val,index) {
this.active = index
this.$router.push({
name: val.name,
params:{id:this.$route.params.id}, //给其他子页面传递id
query:this.$route.query//其他自定义参数
})
},

这样访问子页面时路径就变成’#/Adetail/C/xxxxxxxxxxxxxxxx’
以上处理基本解决掉方案一问题

后续需要根据page A 数据的其他属性值决定菜单显示,所以在配置跳转时配置要传递的属性key
在跳转时,根据KEY获取值,组装成都对象,放在query中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//配置:
{title: '名称', value: 'dbname', display: true,link: {path: 'databaseDetail_outline', key: 'dbinstance_id',query:['dbtype.dbtype']}}
link属性即与跳转相关的配置,
path:跳转路由名
key:以数据哪个属性当id,字符串
query:需要传递的其他参数,字符串数组
//列表点击跳转
jump (item, val) {
//根据key获取值,key对应的可能不是子属性,而是孙属性,或者更深层次属性,valueFromExpression函数用于获取深层属性值
let id = valueFromExpression(item, val.link.key)
let router = {
name: val.link.path,
params: {id: id}
}
if(val.link.query){//根据key值组装传参对象
router.query = val.link.query.map(key=>{return {[key]:valueFromExpression(item, key)}}).reduce((result,item)=>{return Object.assign(result,item)},{})
}
this.$router.push(router)
},

因为打算将路由页面page A-detail-router作为公共页面,page A同级页面的其他页面也可以使用
所以将判断子菜单是否显示的逻辑放在跳转的那个子页面

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
//page A-detail-router
<template>
<div class="subcontainer" v-if="submenuConfig">
<div class="submenu">
<!-- <div class="subTitle" @click="goBack()">{{submenuConfig.title}}</div> -->
<div class="subTitle" @click="goBack()"><i class="fa fa-chevron-left"></i></div>
<template v-for="(item,index) in submenuConfig.submenu">
<div v-if='item.show' @click="gotoPage(item,index)" :key="index" class="subbtn" :class="active===index?'subactive':''">
<a>{{item.label}}</a>
</div>
</template>
</div>
<div class="content">
<router-view></router-view>
</div>
</div>
</template>
<script>
import submenuText from '../../../common/submenu'
export default {
name: 'submenu_detail',
data() {
return {
submenuConfig:null,
active:0
}
},
mounted() {
let routeName = this.$route.name
this.submenuConfig = submenuText[routeName.split('_')[0]] //获取菜单配置
this.active = this.submenuConfig.submenu.findIndex((item)=>{return item.name === routeName})
},
methods: {
gotoPage (val,index) {
this.active = index
this.$router.push({
name: val.name,
params:{id:this.$route.params.id},
query:this.$route.query
})
},
goBack(){
this.$router.push({
name:this.$route.meta.previousMenu
})
}
},
components: {
}
}
</script>
<style lang="less" scoped>
.subcontainer{
display: flex;
flex-grow: row nowrap;
justify-content: space-between;
align-content:stretch;
margin: -20px;
.submenu{
display: flex;
flex-flow: column;
align-content: center;
width: 200px;
flex:none;
background-color: #EAEDF1;
height: 100%;
position: absolute;
.subTitle{
//内容为标题时样式
// font-weight: bold;
// text-indent: 20px;
// height: 70px;
// line-height: 70px;
// background: #D9DEE4;
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
font-weight: bold;
height: 70px;
line-height: 70px;
background: #D9DEE4;
font-size: 20px;
color: #546478;
text-align: center
}
.subTitle:hover{
color: #0080FF;
}
.subbtn{
width: 100%;
font-size: 15px;
text-align: center;
padding: 10px 0px;
a{
color:#000;
}
}
.subbtn:hover{
background-color: #f8f8f8;
background: #f8f8f8;
}
.subactive{
background-color: #fff;
background: #fff;
}
}
.content{
flex:1;
padding: 20px;
margin-left: 200px;
}
}
</style>

//子菜单页面
mounted() {
if(this.$route.query['dbtype.dbtype'] != 'mysql'){
this.$parent.submenuConfig.submenu[1].show = false
}else{
this.$parent.submenuConfig.submenu[1].show = true
}
this.getDetail(this.$route.params.id)
},
My Little World

webpack应用

发表于 2019-03-10

基本配置

1
2
3
4
5
6
7
8
const path = require('path');
module.export = {
entry:'./main.js',
output:{
filename:'bundle.js',
path:path.resolve(__dirname,'./dist')
}
}

接入babel

配置.babelrc

需要额外配置.babelrc文件,该文件是一个JSON格式文件
plugins属性,告诉Babel要使用哪些插件,这些插件可以控制如何转换代码
babel-plugin-transform-runtime:减少冗余代码
babel-runtime:可以导入babel-plugin-transform-runtime依赖的辅助函数
以上二者需要配套使用
presents属性,告诉Babel要转换的源码使用了那些新的语法特性,其值是一组plugins数组,
每一项,
如果不需要配置参数则使用字符串,表示插件名;
如果需要配置参数则使用数组,数组第一项为插件名,第二项为配置项组成的对象;
可以分为三大类:
1.已经被写入ES标准的特性:ES2015,ES2016,ES2017,Env (包含当前所有ES标准里的新特性)
2.社区提出但还未写入ES标准的特性:stage0,stage1,stage2,stage3,stage4,分别表示被纳入es标准的进度
3.特定场景下的语法特性,和es标准没有关系,例如要支持JSX,则使用babel-preset-react

1
2
3
4
{
"plugins":[["transform-runtime",{"polyfill":false}]]
"presents":[["es2015",{"modules":false}],"stage-4","react"]
}

webpack配置

Babel是转换代码功能,所以要配置相应loader

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

安装依赖模块

1
2
3
npm i -D babel-core babel-loader //webpack配置相关依赖
npm i -D babel-preset-env
npm i -D babel-preset-* //配置文件中的相关依赖

使用TypeScript

配置 tsconfig.json文件

1
2
3
4
5
6
7
8
9
"compilerOptions":{
"module":"commonjs",//编译的代码采用的模块规范
"target":"es5",//编译出的代码采用es哪个版本
"sourceMap":true,//输出Source Map 以方便调试
"importHelpers":true//禁止辅助函数重复出现在多个文件
},
"exclude":[
"node_modules"
]

webpack配置

使用loader做转换;修改文件查找规则

1
2
3
4
5
6
7
8
9
resolve:{
extension:['.ts','.js']
}
modules:{
rules:[{
test:/\.ts$/,
loader:'awesome-typescript-loader'
}]
}

安装依赖模块

1
npm i -D typescript awesome-typescript-loader

使用Flow检查器

使用语法

1
2
3
4
//@flow /*告诉检查器这个文件需要被检查*/
function squarel(n:number):number{
return n*n
}

使用配置

1.全局使用
npm i -g flow-bin 将可执行文件安装在全局,直接在项目根目录下使用flow命令执行代码检查
2.局部使用
npm i -D flow-bin 在项目中生成可执行文件,仅在某个项目中使用,
在package.json中配置使用命令

1
2
3
"script":{
"flow":"flow"
}

使用 npm run flow 执行代码检查

去除静态语法

采用flow静态语法的js无法在引擎中运行,所要去掉
1.使用flow-remove-types
2.一般使用flow的项目会使用es6,所以集成到babel配置中,
安装依赖模块:npm i -D babel-preset-flow
修改.babelrc,在preset中加入’flow’:”presets”:[…[],”flow”]

使用SCSS

SCSS又叫做SASS,可以使用一定语法编写css样式
转换方式
1.全局单文件转换
全局安装node-sass:npm i -g node-sass
执行命令:node-sass main.scss main.css //main.css即main.scss编译后的css文件

2.使用sass-loader

webpack配置

1
2
3
4
5
6
modules:{
rules:[{
test:/\.scss$/,
use:['style-loader','css-loader','sass-loader']
}]
}

安装依赖模块

npm i -D style-loader css-loader sass-loader //webpack loader
npm i -D node-sass //sass-loader 依赖node-sass

具体处理流程

sass-loader将scss源码转换为css,再将css代码交给css-loader
css-loader会找出css代码中@import 和 url()这样的导入语句,告诉webpack依赖这些资源,同时支持CSS modules,压缩CSS等功能
处理完后将结果给到style-loader
style-loader 会将CSS代码转换成字符串后,注入到js代码中,通过js向DOM增加样式,
如果想将css提取到单独文件中,可再使用ExtractTextPlugin插件提取

使用PostCSS

使用相应的loader 可以为css样式增加兼容浏览器的前缀,可以配置相应的配置文件,兼容下一代CSS语法

配置postcss.config.js文件

1
2
3
4
5
module.exports = {
plugins:[
require('postcss-cssnext') //postcss-cssnext支持下一代css语法编写
]
}

webpack配置

需要先将下一代语法转换成可识别css,同时添加前缀

1
2
3
4
5
6
modules:{
rules:[{
test:/\.scss$/,
use:['style-loader','css-loader','postcss-loader']
}]
}

安装依赖模块

npm i -D style-loader css-loader postcss-loader //webpack loader 依赖
npm i -D postcss-cssnext // postcss-loader 依赖

使用react

借助Bable

1.修改配置文件.babelrc,在preset中加入’react’:”presets”:[…[],”react”]
2.安装依赖模块
npm i -D react react-dom//react 基础依赖
npm i -D bable-preset-react //bable转换依赖

借助typescript

1.修改配置文件tsconfig.json

1
2
3
4
"compilerOptions":{
...,
"jsx":"react"//typescript本身支持jsx,开启JSX,支持react
},

2.修改webapck配置文件

1
2
3
4
5
6
7
8
9
resolve:{
extension:['.ts','.tsx',.js'] //typescript 原生支持jsx语法,只不过jsx语法文件后缀必须是tsx
}
modules:{
rules:[{
test:/\.tsx?$/,//同时匹配ts,tsx后缀文件
loader:'awesome-typescript-loader'
}]
}

3.安装依赖模块
npm i -D react react-dom//react 基础依赖
npm i -D @types/react @types/react-dom //react react-dom对应的ts接口描述模块,用于编译react,react-dom

使用VUE

引入

1.修改webapck配置文件

1
2
3
4
5
6
modules:{
rules:[{
test:/\.vue$/,
use:['vue-loader']
}]
}

2.安装依赖模块
npm i -S vue //vue框架依赖
npm i -D vue-loader css-loader vue-template-compiler //构建所需依赖
歌loader作用
vue-loader:解析转换.vue文件,提取其中的逻辑代码script样式代码style以及html模板template,然后交给对应的loader去处理
css-loader:加载vue-loader提取的CSS代码
vue-template-compiler:将vue-loader提取的HTML模板编译成对应可执行的js代码。

构建使用ts编写的vue

1.配置 tsconfig.json文件

1
2
3
4
5
6
"compilerOptions":{
"target":"es5",//构建es5版本的js,与VUE浏览器支持保持一致
"module":'es015',//使用es2015模块化格式
"strict":true,//开启严格模式,对this上数据属性进行严格推断
"moduleResolution":'node'
},

2.webpack配置

1
2
3
4
5
6
7
8
9
10
11
12
resolve:{
extension:['.ts','.js','.vue','json'] //增加对.ts,.vue文件支持
modules:{
rules:[{
test:/\.ts$/,//匹配ts后缀文件
loader:'ts-loader',
exclude:/node_modules/,
options:{
appendTsSuffixTo:[/\.vue$/] //让tsc将vue文件当成一个ts模块去处理,以解决module not found的问题,tsc本身不会处理.vue结尾文件
}
}]
}

3.新增文件vue-shims.d.ts
用以ts支持.vue结尾文件,可识别import 语句导入的.vue文件

1
2
3
4
declare module "*.vue"{
import Vue from "vue";
export default Vue
}

4.vue文件编写
script 部分

1
2
3
4
5
6
7
8
9
10
<script lang='ts'>//lang指明代码语法为ts
improt Vue from "vue"
export default Vue.extend({ //Vue.extend启用ts类型推断
data(){
return{
msg:'hello world'
}
}
})
</script

5.安装依赖
npm i -D ts-loader typescript

单页自动生成html

利用模板文件和插件生成

模板文件

模板文件用于描述哪些资源需要被以某种方式加入到输出的HTML文件中
资源链接URL字符串里问号前面的部分表示资源内容来自哪里,后面的参数表示这些资源注入的方式,可用&链接
_inline:资源是要引入动态资源
_dist:只在生产环境下才引入该资源
_dev:只在开发环境才引入该资源
_ie:只在IE浏览器中引入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html lang="en">
<head>
<meta charset="utf-8">
<!--导入chunck app 中的css代码,可知要将css单独打包一个文件-->
<link rel="stylesheet" href="app?_inline">
<!--在这里引入/google_analytics的js代码-->
<script src="./google_analytics.js?_inline"></script>
</head>
<body>
<div id='app'></div>
<!--导入chunck app 中的js代码-->
<script type="text/javascript" src="app"></script>
</body>
</html>

webpack配置

使用web-webpack-plugin的webplugin自动生成index.html

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
const {WebPlugin} = require('web-webpack-plugin')
plugins:[
new WebPlugin({//自动生成index.html文件
template:'./template.html',//模板文件
filename:'index.html'//输出文件名
}),
new ExtractTextPlugin({
filename:`[name]_[contenthas:8].css` // 为输出的css文件名称加上Hash值
}),
new DefinePlugin({
'process.env':{//定义NODE_ENV环境变量为production,去除源码中只有开发时才需要的部分
NODE_ENV:JSON.stringify('production')
}
}),
new UglifyJsPlugin({//压缩输出的js代码
beautify:false,//最紧凑的输出
comments:false,//删除所有注释
compress:{
warnings:false,//删除没有用到的代码时,不发出警告
drop_console:true,//删除所有console语句,兼容IE
collapse_vars:true,//内嵌已定义但只用到了一次的变量
reduce_vars:true//提取出出现多次但没有定义成变量去引用的静态值
}
})

]

自动生成多个单页

需要解决的问题
1.要能够将公共代码提取出来,并能够注入到每个单页应用中
2.模板文件要支持注入公共文件和各个单页独自依赖的资源

模板文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html lang="en">
<head>
<meta charset="utf-8">
<!--在这里注入该页面所依赖但没有手动导入的CSS-->
<!--STYLE-->
<!--在这里引入/google_analytics的js代码-->
<script src="./google_analytics.js?_inline"></script>
</head>
<body>
<div id='app'></div>
<!-- 在这里注入该页面所依赖但没有手动导入的JS-->
<!--SCRIPT-->
</body>
</html>

此时模板文件被当作项目中所有单页的模板,因此不能直接写Chunck名称去引入资源,
因为需要被注入当前页面的Chunck名称不固定,每个单页都会有自己的名称
的作用在于保证该页面所依赖的资源都会被注入生成的HTML模板中
如果不存在,就注入到head标签最后
如果不存在,就注入到body标签最后

webpack配置

使用web-webpack-plugin的autoWebPlugin自动生成多个单页的index.html文件
但是对目录结构有要求,即所有单页各自的入口文件和依赖资源组成各自的一个文件夹,
多个文件夹放在同一目录A下,其他公共资源以及模板文件,webpack配置文件放在与A目录同级目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const {AutoWebPlugin} = require('web-webpack-plugin')
const autoWebPlugin = new AutoWebPlugin('pages',{ //pages 为各个单页的父目录A
template:'./template.html',//模板文件
postEntry:['./common.css'],//所有页面都依赖这份通用的CSS样式文件
commonChunk:{
name:'common',//提取公共代码chunk的名称
}
})
module.exports={
//AutoWebPlugin会为寻找到的所有单页生成对应的入口配置,
//autoWebPlugin.entry可以获取所有由autoWebPlugin生成的入口配置
entry:autoWebPlugin.entry({//可加入额外需要的Chunk入口}),
plugins:[autoWebPlugin]
}

构建基于react的同构应用

同构应用:写一份代码可同时在浏览器和服务器中运行的应用
能在服务器运行的原理核心是虚拟DOM
虚拟DOM好处:
1.操作DOM树是高耗时操作,可通过DOM diff算法找到两个不同Object的最小差异,得出最小的DOM操作
2.虚拟DOM在渲染时不仅可以通过操作DOM树表示结果,也可以有其他表示方式,例如将虚拟DOM渲染成字符串(服务器渲染)
或者渲染成手机APP原生UI组件(react Native)

服务器端构建配置

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
const path = require('path')
const nodeExternals = require('webpack-node-externals')
module.exports = {
entry:'./main_server.js'
target:'node',//目标运行环境是node.js,源码依赖的nodejs原生模块没必要打包进去
externals:[nodeExternals()],//不打包node_modules中的第三方组件
output:{
libraryTarget:'commonjs2',//输出CommonJS2规范,供nodejs的http服务器代码调用
fillname:'bundle.server.js',
path:path(resolve(__dirname,'./dist'))
},
module:{
rules:[
{
test:/\.js$/,
use:['babel-loader'],
exclude:path.resolve(__dirname,'node_modules'),
},
{
test:/\.css/,
use:['ignore-loader'],//css文件不能打包到服务端代码,影响服务端渲染性能
}
]
}

}

文件准备

一个仅包含根组件代码,不能包含渲染入口代码,而且需要导出根组件以供渲染入口调用的文件 rootComponent.js

1
2
3
4
5
6
7
import react,{Component} from 'react';
import 'main.css'
export class RootComponent extends Component{
render(){
return <h1>hello world </h1>
}
}

不同环境渲染入口文件,一个环境一个
main_server.js

1
2
3
4
5
6
import react from 'react';
import RootComponent from './rootComponent'
import {renderToString} from 'react-dom/server' //计算表示虚拟DOM的HTML形式字符串
export function render(){
return renderToString(<RootComponent>)
}

main_browser.js

1
2
3
4
import react from 'react';
import RootComponent from './rootComponent'
import {render} from 'react-dom' //操作浏览器DOM树展示出结果
render(<RootComponent>,window.document.getElementById('app'))

http.server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express')
const {render } = require('./dist/bundle_server')
const app = express();
app.get('/',function(req.res){
res.send(`
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div id='app'>${render()}</div>
<!--导入WEBPACK输出的用于浏览器端渲染的js文件-->
<script src='./dist/bundle_browser.js'></script>
</body>
</html>
`)
})
app.use(express.static('.'));
app.listen(3000,function(){
console.log('app is listening 3000')
})

执行命令

webpack –config webpac_server.config.js 得到./dist/bundle_server.js
webpack –config webpac.config.js 得到./dist/bundle_browser.js
node ./http_server.js 启动 http 服务器,访问’localhost:3000’看到服务器返回了html

基于react 的Electron应用

Electron 可以使用开发web应用的技术去开发跨平台的桌面应用,例如Atom,VSCode
是nodejs和Chromium浏览器的结合体,用Chromium浏览器显示出的WEB页面作为应用的GUI,通过Nodejs和操作系统交互
当操作一个Electron应用的一个窗口时,实际是在操作一个网页,当操作需要操作系统完成时,网页会通过Nodejs和操作系统交互
优点
1.降低了开发门槛,只需掌握网页开发技术和Nodejs,大量Web开发技术和现成库可以复用于Electron
而且由于Electron环境内置了浏览器和Nodejs的API,在开发网页时除了可以使用浏览器提供的API,还可以会用Nodejs的API
2.由于Chromium浏览器和Nodejs都是跨平台的,所有Electron能做到在不同操作系统运行一份代码

构建

Electron 应用每个窗口对应一个网页,所以相当于需要构建多单页面应用
在网页JS代码中可能会调用Nodejs原生模块或者Electron模块,输出的代码依赖这些模块但由于这些模块都是内置支持的,
所有构建出的代码不能将这些模块打包进去
由于webpack内置了对Electron的支持,只需要告诉webpack我要在electron环境里运行就可以实现

1
target:'electron-renderer'

运行

安装electron执行环境到项目中
npm i -D electron

在项目目录下执行electron ./就可以启动桌面应用

构建Npm模块

要求

1.源码若采用es6编写需要转换成es5,并且要遵守commonjs规范,同时提供Source Map方便调式

解决:
使用Babel将es6转换成es5
通过开启devtool:’source-map’输出Source Map以发布调试
设置output.libraryTarget = ‘commonjs2’ 实现输出代码符合CommomJS2规范
2.若为UI组件,则依赖的其他资源文件如css文件也需要包含的发布的模块中

解决:通过css-loader,extract-text-webpack-plugin实现,将css打包到单独文件

3.尽量减少代码冗余,(例如,Babel将es6转换成es5时,会注入一些辅助函数,例如实现class,extend语法的辅助函数,Babel会在每个输出文件中中内嵌依赖的辅助函数,多文件依赖的话,就会造成辅助函数重复出现,造成代码冗余)减少发布出去的组件的代码文件大小

解决:通过引入相同函数解决重复代码
使用babel-plugin-transform-runtime 将嵌入辅助函数代码转成引入辅助函数
引入bable-runtime模块用于提供辅助函数

4.发布出去的组件代码中不能含有其依赖的模块代码,例如react,babel-runtime,而是让用户可选择性的安装,否则可能在其他模块也依赖相同模块时,造成重复打包

解决:配置externals将外部环境提供的模块屏蔽掉,不进行打包

webpack配置

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
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')

module.exports = {
entry:'./src/index.js'
output:{
fillname:'index.js',
path:path.resolve(__dirname,'lib'),
libraryTarget:'commonjs2',
},
externals:/^(react|babel-runtime)/,
module:{
rules:[
{
test:/\.js$/,
use:['babel-loader'],
exclude:path.resolve(__dirname,'node_module'),
},
{
test:/\.css/,
use:ExtractTextPlugin.extract({
use:['css-loader']
})
}]
},
plugins:[
new ExtractTextPlugin({
fillname:'index.css'
})
],
devtool:'source-map'
}

.babellrc文件

1
2
3
4
5
6
7
8
9
10
{
'plugins':[
[
'transform-runtime',//默认自动注入ES6 API的polyfill
{
'polyfill':false//防止使用者在其他地方注入其他polyfill库,所以关闭注入polyfill功能
}
]
]
}

发布到Npm

修改package.json入口文件为打包后的文件

1
2
main:'lib/index.js',//webpack使用于构建不可分割的NPM模块,不能保持同源码结构一致例如如果打包lodash,会将所有工具函数打包进去,不适合仅用几个工具函数的场景
'jsnext:main':'src/index.js'//指出采用ES6编写的模块入口文件位置,便于实现Tree Sharking

离线应用 service workers打包

service workers了解

问题

1.如何生成sw.js文件
2.sw.js文件中的cacheFileList变量(代表被缓存文件的URL),需要根据输出文件列表所对应的URL来决定,不能写成静态值

解决

使用serviceworker-webpack-plugin,根据自定义sw.js生成含有输出文件列表的sw.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
const path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const {WebPlugin} = require('web-webpack-plugin')
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')

module.exports = {
entry:{
app:'./main.js' //Chunk app 的js执行入口文件
},
output:{
fillname:'[name].js',
publicPath:'',
},
module:{
rules:[
{
test:/\.css/,//增加对CSS文件支持,
use:ExtractTextPlugin.extract({//提取到单独文件
use:['css-loader']//压缩CSS代码
})
}]
},
plugins:[
//一个WebPlugin对应一个HTML
new WebPlugin({
template:'./template/html',//HTML模板文件所在的文件路径
fillname:'index.html'//输出的HTML文件名称
}),
new ExtractTextPlugin({
fillname:'[name].css'
}),
new ServiceWorkerWebpackPlugin({
//自定义的sw.js文件所在路径
//ServiceWorkerWebpackPlugin会将文件列表注入生成的sw.js
entry:path.join(__dirname,'sw.js')
})
],
devServer:{
//Service workers依赖HTTPs,使用devServer提供https功能
https:true
}
}

构建出的sw.js文件会在头部注入一个变量serviceWorkerOption.assets到全局,里面存放着所有需要被缓存的文件的URL列表
因此需要将sw.js文件中写成静态值的cacheFileList替换成serviceWorkerOption.assets
var cacheFileList = global.serviceWorkerOption.assets

安装

npm -i -D web-webpack-plugin serviceworker-webpack-plugin

代码检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
module:{
rules:[
{
test:/\.js$/,
exclude:/node_module/,//不检查node_module下文件
loader:'tslint-loader',//整合typeScript代码检查
enforce:'pre'//将执行顺序放到最前面,防止其他Loader将处理后的代码交给tslint-loader检查
},{
test:/\.js$/,
exclude:/node_module/,//不检查node_module下文件
loader:'eslint-loader',//整合eslint检查代码
enforce:'pre'//将执行顺序放到最前面,防止其他Loader将处理后的代码交给tslint-loader检查
}]
},
plugin:[
new StyleLintPlugin() //整合stylelint,检查css代码,可以解析SCSS,Less
]
}

导致问题

1.执行检查步骤计算量大,或导致webpack构建变慢
2.整合代码检查到webpack后,输出的错误信息是通过行号来定位错误的,没有编辑器集成显示错误直观

解决

1.将代码检查步骤当道代码提交时,即在代码提交前调用以上检查工具去检查代码,只有检查都通过时才提交代码,这样保证仓库内代码都经过检查
2.使用集成了代码检查功能的编辑器,让编辑器实时,直观的显示错误
安装 npm i -D husky 接入git hook,通过git 的hook功能做到在提交代码前触发执行,husky会通过Npm Script Hook自动配置好HOOK
但需要在package.json定义脚本,如下

1
2
3
4
5
6
7
{
"script":{
"precommit":"npm run lint",//git commit 前执行的脚本
"prepush":'lint',//git push前会执行的脚本
"lint":'eslint && stylelint'//调用eslint,stylelint检查工具
}
}

在nodejs中使用

通过调用Webpack的API,执行构建

一次构建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const webpack = require('webpack')
//import webpack from 'webpack'

1.使用模块方式一
webpack({
//webpack配置,和webpack.config.js一样
},(err,stats)=>{
if(err || stats.hasErrors()){
//构建过程出错
}
//成功执行完构建
})
2.使用模块方式二
const config = require('./webpack.config.js')
webpack(config,callback)

启动监听模式

1
2
3
4
5
6
7
8
9
10
11
12
13
//不给webpack传递callback就会返回compiler实例,用于控制启动,而不是像上面那样立即启动
const compiler = webpack(config)
//调用compiler.watch并以监听模式启动,返回watching用于关闭监听
const watching = compiler.watch({
//watchOptions
aggregateTimeout:300
},(err,stats)=>{
//每次因文件发生变化而重新执行完/构建后
})
//调用watching.close关闭监听
watching.close(()=>{
//在监听关闭后
})

加载图片相关的loader

1.file-loader
将js和CSS中导入图片的地址替换成 webpack输出文件的地址,输出文件名是根据内容计算出的HASH值

1
2
3
4
rules:[{
test:/\.png$/,
use:['fill-loader']
}]

2.url-loder
将图片转base64直接注入到引入的地方,
一般利用url-loder将网页需要用到的小图片资源注入代码中,以减少加载次数,为一个很小图片而2新建一次HTTP连接不划算
如果图片体积太大会导致js,CSS文件过大而带来网页加载缓慢的问题

1
2
3
4
5
6
7
8
9
10
11
12
rules:[{
test:'/\./png$/',
use:[{
loader:'url-loader',
options:{
//30KB以下文件采用url-loader,控制文件大小
limit:1024*30,
//否则采用file-loader,默认值是file-loader
fallback:'file-loader'
}
}]
}]

还可以以下方式优化,同样适用于其他二进制类型的资源,如PDF,SWF
A.通过imagin-webpack-plugin压缩图片
B.通过webpack-spritesmith插件制作雪碧图

以上两个loader都可用于处理svg图片,但svg文件是文本格式文件,还有其他方法

3.raw-loader
可以将文本文件内容读取出来,注入js/CSS中
由于会直接返回svg的文本内容,并且无法通过CSS展示SVG的文本内容,因此采用该loader后无法在CSS中导入SVG

1
2
3
4
rules:[{
test:/\./svg$/,
use:'raw-loader'
}]

4.svg-inline-loader
类似raw-loader,但会分析SVG内容,去除其中不必要的部分代码,以减少SVG文件大小,相当于增加了对SVG的压缩能力

1
2
3
4
rules:[{
test:/\.svg$/,
use:['svg-inline-loader']
}]

DevServer 实现

webpack-dev-server本身基于webpack-dev-middleware和expressjs,而webpack-dev-middleware是一个express.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
53
54
55
const express = require('express')
const wepack = require('webpack')
const webpackMiddleware = require('webpack-dev-middleware')

const config = require('./webpack.config.js')
const app = expree()
const compiler = webpack(config)
app.use(webpackMiddleware(compiler,{
//在webpack-dev-middleware支持的所有配置项中
//只有publicPath属性为必填项,其他都是选填项

//webpack输出资源绑定HTTP服务器上的根目录
//同WEBPACK配置中的publicPath
publicPath:'/assets/',

//不输出info类型的日志到控制台,只输出warn和error类型的日志
noInfo:false,
//不输出任何类型的日志到控制台
quiet:false,
//切换到懒惰模式,意味着不监听文件的变化,只会在有请求时再编译对应的文件,适合页面很多的项目
lazy:true,
//watchOptions,只在非懒惰模式下才有效
watchOptions:{
aggregationTimeout:300,
poll:true
},

//默认的URL路径,默认是'index.html'
index:'index.html',
//自定义HTTP头
headers:{'X-Custom-Header':'yes'},
//为特定后缀的文件添加HTTP mimeTypes,作为文件类型映射表
mimeTypes:{'text/html':['phtml']},

//统计信息输出样式
stats:{
colors:true
},
//自定义输出日志的展示方法
reporter:null,
//开启或关闭服务端渲染
serverSideRender:false

}))
//webpackMiddleware函数返回一个Expressjs中间件,该中间件有俩个功能
//1.接收来自webpack compiler实例输出的文件,但不会将文件输出到硬盘中,而会保存在内存中
//2.在express.js上注册路由,拦截HTTP收到的请求,根据请求路径响应对应文件内容

//webpack-dev-middleware没有模块热替换功能,但Devserver有,
//可通过webpack-hot-middleware中间件来支持模块热替换,响应用于替换老模块的资源
app.use(require('webpack-hot-middleware')(compiler))
//将项目根目录作为静态资源目录,用于服务器HTML文件
app.use(express.static('.'))

app.listen(3000)

响应模块热替换功能还需要作如下配置
1.修改webpack.config.js文件,加入HotModuleReplacementPlugin插件
相当于执行 webpack-dev-server –hot工作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HotModuleReplacementPlugin= require('webpack/lib/HotModuleReplacementPlugin')
module.exports={
entry:[
'webpack-hot-middleware/client',//为了支持模块热替换注入代理客户端
'./src/main.js'
],
output:{
filename:'bundle.js'
},
plugin:[
//为了支持模块热替换,生成.hot-update.json文件
new HotModuleReplacementPlugin(),
]
}

2.修改入口文件main.js,加入替换逻辑

1
2
3
4
//在文件末尾加入
if(module.hot){
moudule.hot.accept()
}

3.安装以上配置中用到的依赖
npm i -D webpack-dev-middleware webpack-hot-middleware express

My Little World

react Native 基础知识

发表于 2019-03-05

常用标签

Text:用来显示文本
View:相当于div或者span这样的容器,常用作其他组件的容器,来帮助控制布局和样式
Image:显示图片,属性source指定图片地址,也可以使用style属性控制尺寸

知识点

props:子组件通过this.props拿到父组件使用子组件时传递进来的属性值
state:自定义组件控制内部逻辑的变量,同react的state,在constructor里面初始化
StyleSheet:使用StyleSheet.create()创建样式,相当于style标签,不过使用时在style中使用

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
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, Text, View } from 'react-native';

export default class LotsOfStyles extends Component {
render() {
return (
<View>
<Text style={styles.red}>just red</Text>
<Text style={styles.bigBlue}>just bigBlue</Text>
<Text style={[styles.bigBlue, styles.red]}>bigBlue, then red</Text>
<Text style={[styles.red, styles.bigBlue]}>red, then bigBlue</Text>
</View>
);
}
}

const styles = StyleSheet.create({
bigBlue: {
color: 'blue',
fontWeight: 'bold',
fontSize: 30,
},
red: {
color: 'red',
},
});

在style属性中可以直接写驼峰式CSS属性进行样式调整
但设置width和height是不带单位的,React Native 中的尺寸都是无单位的,表示的是与设备像素密度无关的逻辑像素点。

Text

相当于span,但可以被’\n’换行
用作子元素时,
如果父元素为Text时,多个Text子元素尽可能放一行,一行装不下时,自动换行,子元素Text标签会继承父元素Text一部分样式
如果父元素为View时,每个Text子元素成为flex布局的一个块,当容器不够宽时,每个块自动换行,块与块之间不影响
必须把文本放在Text组件中,不能直接放在View中

属性

selectable:是否可以长按选择文本,以便复制和粘贴
selectionColor[Andorid]:选中时高亮颜色
suppressHighlighting[IOS]:设为true时,当文本被按下会没有任何视觉效果。默认情况下,文本被按下时会有一个灰色的、椭圆形的高光
ellipsizeMode:表示当 Text 组件无法全部显示需要显示的字符串时如何用省略号进行修饰
————head : 从文本内容头部截取显示省略号。例如: “…efg”
————middle : 在文本内容中间截取显示省略号。例如: “ab…yz”
————tail : 从文本内容尾部截取显示省略号。例如: “abcd…”
————clip : 不显示省略号,直接从尾部截断。
numberOfLines:文本过长时,最多折叠多少行,执行ellipsizeMode设置的效果
onLayout:加载时或者布局变化以后调用,参数为:{nativeEvent: {layout: {x, y, width, height}}}
onLongPress:当文本被长按以后调用此回调函数
onPress:当文本被点击以后调用此回调函数。
llowFontScaling:制字体是否要根据系统的“字体大小”辅助选项来进行缩放。默认值为true。

TextInput

有些属性仅在multiline为true或者为false的时候有效,例如当multiline=false时,为元素的某一个边添加边框样式(例如:borderBottomColor,borderLeftWidth等)将不会生效
在安卓上长按选择文本会导致windowSoftInputMode设置变为adjustResize,这样可能导致绝对定位的元素被键盘给顶起来。要解决这一问题你需要在AndroidManifest.xml中明确指定合适的windowSoftInputMode( https://developer.android.com/guide/topics/manifest/activity-element.html )值,或是自己监听事件来处理布局变化。

属性

placeholder:同input
maxLength:限制文本框中最多的字符数。使用这个属性而不用JS逻辑去实现,可以避免闪烁的现象。
multiline:如果为true,文本框中可以输入多行文字。默认值为false。注意安卓上如果设置multiline = {true},文本默认会垂直居中,可设置textAlignVertical: ‘top’样式来使其居顶显示
numberOfLines:设置输入框的行数
allowFontScaling:控制字体是否要根据系统的“字体大小”辅助选项来进行缩放。默认值为true。
autoCapitalize:控制TextInput是否要自动将特定字符切换为大写
————characters: 所有的字符。
————words: 每个单词的第一个字符。
————sentences: 每句话的第一个字符(默认)。
————none: 不切换。
autoCorrect:如果为false,会关闭拼写自动修正。默认值是true
autoFocus:如果为true,在componentDidMount后会获得焦点。默认值为false。
blurOnSubmit:如果为true,文本框会在提交的时候失焦。对于单行输入框默认值为true,多行则为false。注意:对于多行输入框来说,如果将blurOnSubmit设为true,则在按下回车键时就会失去焦点同时触发onSubmitEditing事件,而不会换行。
caretHidden:如果为true,则隐藏光标。默认值为false。
clearButtonMode:是否要在文本框右侧显示“清除”按钮。仅在单行模式下可用。默认值为never。
clearTextOnFocus:如果为true,每次开始输入的时候都会清除文本框的内容。
defaultValue:
提供一个文本框中的初始值。当用户开始输入的时候,值就可以改变。在一些简单的使用情形下,如果你不想用监听消息然后更新value属性的方法来保持属性和状态同步的时候,就可以用defaultValue来代替。
editable:如果为false,文本框是不可编辑的。默认值为true
enablesReturnKeyAutomatically:如果为true,键盘会在文本框内没有文字的时候禁用确认按钮。默认值为false。
inlineImageLeft:指定一个图片放置在左侧。图片必须放置在/android/app/src/main/res/drawable目录下,经过编译后按如下形式引用(无路径无后缀)

1
2
3
<TextInput
inlineImageLeft='search_icon'
/>

inlineImagePadding:给放置在左侧的图片设置padding样式。
keyboardAppearance:指定键盘的颜色。
keyboardType:决定弹出何种软键盘类型

响应

onBlur:当文本框失去焦点的时候调用此回调函数。
onChange:当文本框内容变化时调用此回调函数。回调参数为{ nativeEvent: { eventCount, target, text} }
onChangeText:当文本框内容变化时调用此回调函数。改变后的文字内容会作为参数传递。
onEndEditing:当文本输入结束后调用此回调函数。
onKeyPress:当一个键被按下的时候调用此回调。传递给回调函数的参数为{ nativeEvent: { key: keyValue } },其中keyValue即为被按下的键。会在onChange之前调用。注意:在Android上只有软键盘会触发此事件,物理键盘不会触发。
onSubmitEditing:此回调函数当软键盘的确定/提交按钮被按下的时候调用此函数。如果multiline={true},此属性不可用。

Button

样式单一,可能不适合统一UI样式

属性

title:按钮名
color:文本的颜色(iOS),或是按钮的背景色(Android)
disabled:禁用

响应

onPress:点击触发

TouchableHighlight

用来封装可以点击的元素,来制作按钮或者链接。注意此组件的背景会在用户手指按下时变暗。
使其可以正确响应触摸操作。当按下的时候,封装的视图的不透明度会降低,同时会有一个底层的颜色透过而被用户看到,使得视图变暗或变亮
只支持一个子节点(不能没有子节点也不能多于一个)。如果你希望包含多个子组件,可以用一个View来包装它们

属性

activeOpacity:指定封装的视图在被触摸操作激活时以多少不透明度显示(0到1之间,默认值为0.85)。需要设置underlayColor。
underlayColor:有触摸操作时显示出来的底层的颜色。

响应

onHideUnderlay:底层的颜色被隐藏的时候调用。
onShowUnderlay:当底层的颜色被显示的时候调用。

TouchableNativeFeedback

用来封装可以点击的元素,在用户手指按下时形成类似墨水涟漪的视觉效果
它只支持一个单独的View实例作为子节点

属性

background:决定在触摸反馈的时候显示什么类型的背景,它接受一个有着type属性和一些基于type属性的额外数据的对象。
一般用本组件的几个静态方法来创建这个对象
——————SelectableBackground():创建一个对象,表示安卓主题默认的对于被选中对象的背景
——————SelectableBackgroundBorderless():创建一个对象,表示安卓主题默认的对于被选中的无边框对象的背景
——————Ripple(color: string, borderless: boolean):创建一个对象,当按钮被按下时产生一个涟漪状的背景,你可以通过color参数来指定颜色,如果参数borderless是true,那么涟漪还会渲染到视图的范围之外

ScrollView

一个通用的可滚动的容器,你可以在其中放入多个组件和视图,而且这些组件并不需要是同类型的,适合用来显示数量不多的滚动元素

属性

horizontal:为true时,元素水平排列,默认false,垂直排列
scrollsToTop:当此值为true时,点击状态栏的时候视图会滚动到顶部。默认值为true
indicatorStyle:设置滚动条的样式。默认default 同black,’white’白色滚动条
overScrollMode:覆盖默认的overScroll模式
——————‘auto’ : 默认值,允许用户在内容超出视图高度之后可以滚动视图。
——————‘always’ : 无论内容尺寸,用户始终可以滚动视图。
——————‘never’ : 始终不允许用户滚动视图。
stickyHeaderIndices:一个子视图下标的数组,用于决定哪些成员会在滚动之后固定在屏幕顶端。举个例子,传递stickyHeaderIndices={[0]}会让第一个成员固定在滚动视图顶端。这个属性不能和horizontal={true}一起使用。
scrollEnabled:当值为false的时候,内容不能滚动,默认值为true。注意即便禁止用户滚动,你也仍然可以调用scrollTo来滚动。
showsHorizontalScrollIndicator:为true的时候,显示一个水平方向的滚动条。
showsVerticalScrollIndicator:为true的时候,显示一个垂直方向的滚动条
refreshControl:用于为ScrollView提供下拉刷新功能。只能用于垂直视图,即horizontal不能为true。
pagingEnabled:当值为true时,滚动条会停在滚动视图的尺寸的整数倍位置。这个可以用在水平分页上。默认值为false。注意:垂直分页在Android上不支持。

响应

onScrollBeginDrag:当用户开始拖动此视图时调用此函数
onScrollEndDrag:当用户停止拖动此视图时调用此函数。
onScroll:在滚动的过程中,每帧最多调用一次此回调函数。调用的频率可以用scrollEventThrottle属性来控制。
onMomentumScrollBegin:滚动动画开始时调用此函数。
onMomentumScrollEnd:滚动动画结束时调用此函数
scrollTo(([y]: number),([x]: number),([animated]: boolean),([duration]: number)):滚到指定位置时
scrollToEnd(([options]: {animated: boolean, duration: number}));滚到视图底部

FlatList

用于显示一个垂直的滚动列表,其中的元素之间结构近似而仅数据不同
更适于长列表数据,且元素个数可以增删。和ScrollView不同的是,FlatList并不立即渲染所有元素,而是优先渲染屏幕上可见的元素。

属性

data:数据源
renderItem:返回重复的子组件
ItemSeparatorComponent:行与行之间的分隔线组件。不会出现在第一行之前和最后一行之后。值是一个组件
ListEmptyComponent:列表为空时渲染该组件。
ListHeaderComponent:头部组件
ListFooterComponent:尾部组件。
horizontal:设置为 true 则变为水平布局模式

extraData:如果有除data以外的数据用在列表中(不论是用在renderItem还是头部或者尾部组件中),请在此属性中指定。同时此数据在修改时也需要先修改其引用地址(比如先复制到一个新的 Object 或者数组中),然后再修改其值,否则界面很可能不会刷新。

initialNumToRender:指定一开始渲染的元素数量,最好刚刚够填满一个屏幕,这样保证了用最短的时间给用户呈现可见的内容。注意这第一批次渲染的元素不会在滑动过程中被卸载,这样是为了保证用户执行返回顶部的操作时,不需要重新渲染首批元素

initialScrollIndex:开始时屏幕顶端的元素是列表中的第 initialScrollIndex个元素, 而不是第一个元素。如果设置了这个属性,则第一批initialNumToRender范围内的元素不会再保留在内存里,而是直接立刻渲染位于 initialScrollIndex 位置的元素。需要先设置 getItemLayout 属性。

keyExtractor:此函数用于为给定的 item 生成一个不重复的 key。Key 的作用是使 React 能够区分同类元素的不同个体,以便在刷新时能够确定其变化的位置,减少重新渲染的开销。若不指定此函数,则默认抽取item.key作为 key 值。若item.key也不存在,则使用数组下标。

onEndReachedThreshold:决定当距离内容最底部还有多远时触发onEndReached回调。注意此参数是一个比值而非像素单位
refreshing:在等待加载新数据时将此属性设为 true,列表就会显示出一个正在加载的符号

响应

onEndReached:当列表被滚动到距离内容最底部不足onEndReachedThreshold的距离时调用
onRefresh:如果设置了此选项,则会在列表头部添加一个标准的RefreshControl控件,以便实现“下拉刷新”的功能。同时你需要正确设置refreshing属性
onViewableItemsChanged:在可见行元素变化时调用。可见范围和变化频率等参数的配置请设置viewabilityConfig属性
scrollToOffset():滚动列表到指定的偏移(以像素为单位),等同于ScrollView的scrollTo方法。

SectionList

要渲染的是一组需要分组的数据,也许还带有分组标签的数据
文档

Platform

Platform是一个模块,不是组件,用来进行平台检测
Platform.OS,在 iOS 上会返回ios,而在 Android 设备或模拟器上则会返回android。
Platform.select({ios:{},android:{}}) 可以以 Platform.OS 为 key,从传入的对象中返回对应平台的值
Platform.Version, Android 的 api level,值为数字,ios上为一个表示当前系统版本的字符串
当不同平台的代码逻辑较为复杂时,最好是放到不同的文件里,这时候我们可以使用特定平台扩展名。React Native 会检测某个文件是否具有.ios.或是.android.的扩展名,然后根据当前运行的平台自动加载正确对应的文件。
比如你可以在项目中创建下面这样的组件:

1
2
BigButton.ios.js
BigButton.android.js

然后去掉平台扩展名直接引用:

1
import BigButton from './BigButton';

Image

用于管理 iOS 和 Android 应用中的图片
图片文件的查找会和 JS 模块的查找方式一样,如果有my-icon.ios.png和my-icon.android.png,Packager 就会根据平台而选择不同的文件
可以使用@2x,@3x这样的文件名后缀,来为不同的屏幕精度提供图片,Packager 会打包所有的图片并且依据屏幕精度提供对应的资源,如果没有图片恰好满足屏幕分辨率,则会自动选中最接近的一个图片。
为了使新的图片资源机制正常工作,require 中的图片名字必须是一个静态字符串(不能使用变量!因为 require 是在编译时期执行,而非运行时期执行!)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正确
<Image source={require('./my-icon.png')} />;

// 错误
var icon = this.props.active ? 'my-icon-active' : 'my-icon-inactive';
<Image source={require('./' + icon + '.png')} />;

// 正确
var icon = this.props.active
? require('./my-icon-active.png')
: require('./my-icon-inactive.png');
<Image source={icon} />;
//通过这种方式引用的图片资源包含图片的尺寸(宽度,高度)信息,如果需要动态缩放图片(例如,通过 flex),可能必须手动在 style 属性设置{ width: null, height: null }。

require语法也可以用来静态地加载你项目中的声音、视频或者文档文件,包括.mp3, .wav, .mp4, .mov, .htm 和 .pdf等
注意的是视频必须指定尺寸而不能使用flex样式

要在 App 中显示的图片并不能在编译的时候获得,又或者有时候需要动态载入来减少打包后的二进制文件的大小。这些时候,与静态资源不同的是,需要手动指定图片的尺寸

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
// 正确
<Image source={{uri: 'https://facebook.github.io/react/logo-og.png'}}
style={{width: 400, height: 400}} />
//或者 指定请求参数
<Image
source={{
uri: 'https://facebook.github.io/react/logo-og.png',
method: 'POST',
headers: {
Pragma: 'no-cache',
},
body: 'Your Body goes here',
}}
style={{width: 400, height: 400}}
/>
//或者 引用base64
<Image
style={{
width: 51,
height: 51,
resizeMode: 'contain',
}}
source={{
uri:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADMAAAAzCAYAAAA6oTAqAAAAEXRFWHRTb2Z0d2FyZQBwbmdjcnVzaEB1SfMAAABQSURBVGje7dSxCQBACARB+2/ab8BEeQNhFi6WSYzYLYudDQYGBgYGBgYGBgYGBgYGBgZmcvDqYGBgmhivGQYGBgYGBgYGBgYGBgYGBgbmQw+P/eMrC5UTVAAAAABJRU5ErkJggg==',
}}
/>
// 错误
<Image source={{uri: 'https://facebook.github.io/react/logo-og.png'}} />

读取本地静态图片(使用require(‘./my-icon.png’)语法)则无需指定尺寸,因为它们的尺寸在加载时就可以立刻知道。

ImageBackground

用于设置背景图,把需要背景图的子组件嵌入其中即可,需要设置大小

react-navigation

导航器,控制跳转
资料

InteractionManager

确保在执行繁重工作之前所有的交互和动画都已经处理完毕。

1
2
3
4
5
6
7
8
9
InteractionManager.runAfterInteractions(() => {
// ...需要长时间同步执行的任务...
});
//允许应用注册动画,在动画开始时创建一个交互“句柄”,然后在结束的时候清除它
var handle = InteractionManager.createInteractionHandle();
// 执行动画... (`runAfterInteractions`中的任务现在开始排队等候)
// 在动画完成之后
InteractionManager.clearInteractionHandle(handle);
// 在所有句柄都清除之后,现在开始依序执行队列中的任务

requestAnimationFrame(): 用来执行在一段时间内控制视图动画的代码
setImmediate/setTimeout/setInterval(): 在稍后执行代码。注意这有可能会延迟当前正在进行的动画。
runAfterInteractions(): 在稍后执行代码,不会延迟当前进行的动画。

My Little World

react Native 环境配置

发表于 2019-03-04

其实按照官网一步一步来就好了,也不用翻墙,基本上,就是耗时间
我配置的是window android环境
中文文档
1.安装node
可以直接使用nvm安装,但版本必须是8.或者10.
带来的问题是,nvm切换版本后,npm可能找不到,可以直接用全局安装的yarn代替
npm 与 Yarn 常用命令对比
或者直接将npm文件夹从能用的版本移到不能用的版本

2.python2
官网说不支持Python3.X,因为很早之前安装的python没管,今天一看是3.*,能跑起来

3.JDK
下载地址
我下载的这个
native1
安装过程注意,jdk和jre使用不同文件夹
安装与环境配置
4.Android Studio
按文档步骤安装好后,需要先创建项目再创建虚拟机
构建虚拟机过程文档应该需要翻墙,但其实可以直接百度
构建虚拟机
相当于在PC上调试
5.链接真机测试
数据线链接手机和电脑,在git里跑’react-native run-android’
遇到的问题
1.第一次跑时,出现:unable to load script from assets
解决如下:
第一步:在Android/app/src/main目录下创建一个空的assets文件夹。
第二步:执行

1
react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/

再重新跑react-native run-android
2.摇晃手机reload 后:could not connect to development sever
华为手机andorid版本较低,4.*
点击dev setting,选择Debug server host&port for device,填入PC电脑的IP地址和端口号
3.引入使用native-nivigation和react-native-gesture-handler后运行报错
settings file ‘E:\git\mobileApp\android\settings.gradle’: 3: unexpected char: ‘\’ @ line 3, column 1
解决: android下setiing.gradle,’\’换成‘/’

1
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')

My Little World

webpack配置项

发表于 2019-03-03

webpack在启动后会从Entry里配置的Moudule开始,递归解析Entry依赖的所有Module,每找到一个module,就会根据配置的loader去找对应的转换规则,
对Module进行转换后,在解析当前Module依赖的Module。这些模块会以Entry为单位进行分组,一个Entry及其所有依赖Module被分到一个组,也就是一个Chunk
最后,webpack会将所有Chunk转换成文件输出。webpack会在恰当的时机执行Plugin里定义的逻辑

entry

入口,webpack执行构建的第一步将从Entry开始,可抽象成输入
配置:
方式1: 直接一个文件路径字符串:’./app/entry1’
方式2: 文件路径字符串数组:[‘./app/entry1’,’./app/entry2’]
方式3: 文件路径字符串对象:{A:’./app/entryA’,B:’./app/entryB’}
方式4: 函数,动态导出以上三种形式
方式1,2最后只会有一个文件会被导出,即导出一个Chunk,名为main
方式3导出多个Chunk,名称分别对应对象KEY值

output

输出结果,在webpack经过一系列处理并得到最终想要的代码后输出结果
配置:

filename

输出文件名
方式1:静态,名字写死,如bundle.js,适用于只有一个导出Chunk,一个输出文件
方式2:利用Chunk内置变量,动态拼接生成文件名,例如’[name].js’
Chunk内置变量有:id,name,hash(name的hash,可指定长度,例如[hash:8]),chunkhash(chunk内容的hash)
相关:ExtractTextWebpackPlugin使用contenthash表示文件内容hash,但提取的内容为代码本身,不是一组模块组成的chunck内容

chunkFilename

无具体入口的chunk输出的文件名
例如通过commonChunkPlugin生成的文件,或者使用import动态加载生成的文件等
因为都是chunk输出文件,所以内置变量同filename的chunk

path

输出文件的存放目录
配置:绝对路径的字符串
内置变量:仅有一个,hash,代表一次编译操作的hash值

publishPath

配置发布到线上资源的URL前缀
配置:字符串
内置变量:仅有一个,hash,代表一次编译操作的hash值

crossOriginloading

配置异步插入的script标签的crossorign值
配置:
anonymous,默认,不带cookie
use-credentials,加载时,带cookie

library

导出库的名称,与libraryTarget配合使用
配置:字符串

libraryTarget

以何种方式导出库
配置:以下字符串
‘var’|’commonjs’|’commonjs2’|’this’|’window’|’global’

libraryExport

在libraryTarget配置’commonjs’|’commonjs2’时,配置导出模块中哪些子模块被导出

module

模块处理规则

rules

数组,每一项是处理同一类文件的相关配置对象

test

处理哪些文件,文件名命中规则,
配置:字符串

include

在什么目录范围内匹配
配置:字符串/字符串数组(每一项是或的关系,只要满足一个条件就会命中)

exclude

排除哪些目录范围,再匹配
配置:字符串/字符串数组(每一项是或的关系,只要满足一个条件就会命中)

use

配置loader
loader可以看作具有文件转换功能的翻译员,告诉webpack在遇到哪些文件时使用哪些loader去加载和转换
配置:数组,每一项可以是是字符串,或者对象
默认执行顺序为数组倒序执行,即最末尾先执行
字符串:loader名称
对象:对于该loader的一些相关配置

loader

配置:字符串,loader名

option

配置:对象,给loader处理函数传入的参数

enforce

更改loader执行顺序
配置:
pre: 放在执行顺序最前面
post:放在执行顺序最后

noparse

功能类似于exclude,排除不需要进行接续处理的文件,
此类文件中不能采用模块化方式编写,即不应包含import,require,define等语句,
否则导致无法在浏览器下执行
配置:字符串,文件路径

parse

更细粒度地配置哪些语法被解析,哪些不被解析,精确到语法层面
{cmd:false,commonjs:false….}

resolve

配置如何寻找模块对应的文件

alias

通过将路径前缀指定别名,进行映射
配置:对象,key为别名,值为实际路径
例:{components:’./src/components’,…}

第三方模块一般会包含两套代码
一套采用comonjs规范的模块代码,这些文件都放在lib目录下,以package.json中指定的react.js为模块入口
一套是将React所有相关代码打包好的完整代码放到一个单独文件中,这些代码没有采用模块化,可以直接执行,
其中非压缩文件(dist/xxx.js)用于开发环境,里面包含检查和警告的代码,压缩的代码(dist/xxx.min.js)用于线上环境
默认情况下,webpack会从入口文件./node_module/xxx/xxx.js开始递归解析和处理依赖的几十个文件,会是一个耗时操作
可以通过配置reaolve.alias,让webpack处理第三方库时,直接使用单独完整的xxx.min.js,从而跳过耗时的递归解析操作

mainFields

决定优先使用第三方模块的哪份代码,按数组顺序查找,使用找到的第一份文件
默认值和target有关,
当target为web或者webwork时,值是[‘browser’,’module’,’main’]
当target为其他值时,值时[‘module’,’main’]
配置:第三方模块导出文件的关键字
例:[‘jsnext:main’,’brower’,’main’]

extensions

导入语句没有带文件后缀时,尝试寻找的后缀代表
配置:后缀字符串数组
例:[‘.ts’,’.js’,’json’]

modules

去哪些目录下寻找第三方模块,
默认node_modules 含义是先去当前目录的./node_module目录下找,如果没有找到,就去上一级目录../node_module中去找
再没有就去../../node_module中找,一次类推
配置:字符串数组
例:[‘.src/components’,’node_modules’],
可以直接在页面中import ‘.src/components’里面的模块,不用再写相对路径

descriptionFiles

配置第三方模块名
配置:字符串数组

enforceExtension

是否强制导入语句中必须带后缀
true:必须带
false:不用必须带

enforceModuleExtension

只对node_module文件夹下模块生效
通常设置false,与enforceExtension:true配合,兼容第三方模块,使用时不加后缀

plugin

扩展插件,在webpack构建流程中的特定时机注入扩展逻辑,来改变构建结果或者做一些自定义的操作
比如将css文件单独打包,或者抽离公共模块
不使用plugin处理css文件的原理大概是将CSS内容用JS字符串存储起来,在网页执行JS时,通过DOM操作,动态向HTML head标签里插入HTML style标签
使用plugin将css抽离,是从打包好的js文件再提取出来
配置:plugin实例数组,使用new操作符调用构造函数,同时将参数传递进去
例:[new commonsChunkPlugin({name:’common’})]

devServer

配置针对使用webpack-dev-server启动webpack时的一些配置,相当于配置webpack-dev-server
devServer 会启动一个http服务器,用于服务网页请求,
同时会启动webpack,将webpack构建的文件保存在内存中,在要访问输出的文件时,必须通过HTTP服务访问,
同时会开启webpack的监听模式,devServer会让webpack在构建出的js代码里注入一个代理客户端用于控制网页,
网页和devServer之间通过webSocket协议通信,以方便devServer主动向客户端发送命令,
devServer在收到来自webpack的文件变化通知时,通过注入的客户端控制自动刷新网页,做到实时预览。
1.由于devServer不会理会配置的output.path属性,所以获取打包文件时,应该依据HTTP获取,不再依据output.path获取
因此要注意使用devServer之后打包文件的获取路径要进行更改。
2.index.html因为脱离js模块化系统,webpack不知道它的存在,故,更改index.html,不会被监听到
3.除了通过重新刷新整个网页来实现预览,devServer还有一种被称作模块热替换的刷新技术。
模块热替换能做到在不重新加载整个网页的情况下,通过将已更新的模块替换老模块,再重新执行一次来实现实时预览。
模块热替换相对于默认的刷新机制能提供更快的响应速度和更好的开发体验。
可以在执行devServer时加上 –hot开启
4.启动webpack时,加上–devtool source-map 可生成souce-map,方便在浏览器中调试源代码

配置
hot是否开启热加载(热替换)
模块热替换原理:
类似于自动刷新,都需要向要开发的网页中注入一个代理客户端来连接DevServer和网页,区别在于热替换时会多出三个用于热替换的文件
在发生文件变化时,会重新生成一个用于替换老模块的补丁文件,补丁文件中会包含新编译的代码,页面会使用新编译的代码
当子模块发生更新时,更新事件会一层一层向上传递,会从根组件传递到main.js,直到有某层文件接收了当前变化的模块,
就会去执行自定义的逻辑,如果事件一直往上抛,到最外层都没有文件接收它,则会直接刷新网页,
最直观的就是修改main.js时,会发生整个页面刷新
而对于.css文件,在使用style-loader处理时会注入用于接收CSS的代码,所以在修改.css文件时,会触发模块热替换

inline
开启实现实时预览,自动刷新;
不开启,使用iframe方式运行开发的网页,需要去localhost:8080/webpack-dev-pack实时预览
historyApiFallback:用于H5History API单页应用开发
contentBase:devServer HTTP服务器文件根目录
headers:在HTTP响应中注入HTTP响应头
host:监听地址
port:监听端口
allowedHosts:只有HTTP请求的HOST在列表中才正常返回
disabledHostCheck:是否关闭用于DNS重新绑定的HTTP请求的HOST检查
https:是否运行在https上
clientloglevel:配置客户端日志等级

target

构建不同运行环境的代码

devtool

有很多选项可配置,选项之间可以随意组合
6个关键配置项:
eval:用eval语句包裹需要安装的模块
source-map:生成独立的Source Map文件
hidden:不在JS文件中指出 Source Map文件的位置,这样浏览器就不会自动加载Source Map
inline:将生成的Source Map转换成BASE64格式内嵌在JS文件中
cheap:在生成的Source Map文件中不会包含列信息,这样计算量更小,输出的Source Map文件更小,同时Loader输出的Source Map不会被采用
module:来自Loader的Source Map被简单的处理成每行一个模块

‘source-map’

仅设置source-map会造成以下两个问题
1.会输出质量最高且最详细的Source Map,会造成构建速度缓慢,特别是开发过程中需要频繁修改时会增加等待时间
2.会将Source Map暴露,若构建发布到线上代码的Source Map暴露等于源码被泄露
解决:
1.开发环境下将dev-tool设置成cheap-module-eval-source-map,因为生成这种Source Map的速度最快,能加速构建,
在开发环境下不会做代码压缩,所以在Source Map中即使没有列信息,也不会影响断点调试
2.生产环境下将dev-tool设置成hidden-source-map,生成最详细的Source Map,但不会将Source Map暴露出去
在生产环境会做代码压缩,一个JS文件只有一行,所以需要列信息

生产环境通常不会将Source Map上传到HTTP服务器让用户获取,而是上传到JS错误错误收集系统,
在错误收集系统上根据Source Map和收集到的JS运行错误堆栈,计算出错误所在源码位置

第三方模块

webpack是默认不会加载第三方模块附带的Source Map文件的,会在转换过程中生成Source Map,
为了让webpack加载这些第三方模块的Source Map,需要使用source-map-loader

1
2
3
4
5
6
7
8
9
rules:[{
test:/\.js$/,
include:[path.resolve(root,'node_module/some-component')],
//加载Source Map时计算量很大,因此要避免让该Loader处理过多的文件,不然会导致构建变慢
use:[source-map-loader],
//要将source-map-loader的执行顺序放到最前面,
//如果在source-map-loader之前有Loader转换了该JS文件,就会导致Source Map映射错误
enforce:'pre'
}]

watch

是否开启监听文件变动模式

watchOption

配置监听规则,在开启监听模式时,才有用

ignore:不监听的文件/文件夹
aggregateTimeout:监听到变化发生后,等多少ms再去执行动作,截流,防止文件更新太快而导致重新编译的频率太快,默认为300ms
poll:判断文件是否发生变化是通过不停的询问系统指定文件有没有变化实现的,这里配置每秒询问多少次

文件监听原理:
默认情况下,webpack会从配置的Entry文件出发,递归出Entry文件依赖的文件,将这些依赖的文件都加入到监听列表中,而不是监听整个项目目录下的文件
然后对列表中每个文件都定时执行检查,定时获取这个文件的最后编辑时间,每次都存下最新的最后编辑时间,如果发现当前获取的和最后编辑时间不一致,
就认为该文件发生了变化,watchOption.poll控制定时检查的周期
文件发生变化后并不会立刻告诉监听者,而是先缓存起来,收集一段时间的变化后,再一次性告诉监听者。watchOption.aggregateTimeout用于配置这个等待时间
这样做的目的就是在编辑代码过程可能会高频地输入文字,导致文件变化的时间高频发生,如果每次都重新执行构建,就会让构建卡死

externals

配置哪些模块不用被打包

resolveloader

配置如何去寻找loader

1…91011…26
YooHannah

YooHannah

260 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces