说实话我刚开始看 webpack 文档的时候,对这 3 个名词云里雾里的,感觉他们都在说打包文件,但是一会儿 chunk 一会儿 bundle 的,逐渐就迷失在细节里了,所以我们要跳出来,从宏观的角度来看这几个名词。
webpack 官网对 chunk 和 bundle 做出了解释[3],说实话太抽象了,我这里举个例子,给大家形象化地解释一下。
首先我们在 src 目录下写我们的业务代码,引入 index.js、utils.js、common.js 和 index.css 这 4 个文件,目录结构如下:
src/
├── index.css
├── index.html # 这个是 HTML 模板代码
├── index.js
├── common.js
└── utils.js
index.css 写一点儿简单的样式:
body {
background-color: red;
}
utils.js 文件写个求平方的工具函数:
export function square(x) {
return x * x;
}
common.js 文件写个 log 工具函数:
module.exports = {
log: (msg) => {
console.log('hello ', msg)
}
}
index.js 文件做一些简单的修改,引入 css 文件和 common.js:
import './index.css';
const { log } = require('./common');
log('webpack');
webpack 的配置如下:
{
entry: {
index: "../src/index.js",
utils: '../src/utils.js',
},
output: {
filename: "[name].bundle.js", // 输出 index.js 和 utils.js
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader, // 创建一个 link 标签
'css-loader', // css-loader 负责解析 CSS 代码, 处理 CSS 中的依赖
],
},
]
}
plugins: [
// 用 MiniCssExtractPlugin 抽离出 css 文件,以 link 标签的形式引入样式文件
new MiniCssExtractPlugin({
filename: 'index.bundle.css' // 输出的 css 文件名为 index.css
}),
]
}
我们运行一下 webpack,看一下打包的结果:
我们可以看出,index.css 和 common.js 在 index.js 中被引入,打包生成的 index.bundle.css 和 index.bundle.js 都属于 chunk 0,utils.js 因为是独立打包的,它生成的 utils.bundle.js 属于 chunk 1。
感觉还有些绕?我做了一张图,你肯定一看就懂:
看这个图就很明白了:
一般来说一个 chunk 对应一个 bundle,比如上图中的 utils.js -> chunks 1 -> utils.bundle.js;但也有例外,比如说上图中,我就用 MiniCssExtractPlugin 从 chunks 0 中抽离出了 index.bundle.css 文件。
module,chunk 和 bundle 其实就是同一份逻辑代码在不同转换场景下的取了三个名字:
我们直接写出来的是 module,webpack 处理时是 chunk,最后生成浏览器可以直接运行的 bundle。
filename 是一个很常见的配置,就是对应于 entry 里面的输入文件,经过webpack 打包后输出文件的文件名。比如说经过下面的配置,生成出来的文件名为 index.min.js。
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
}
}
chunkFilename 指未被列在 entry 中,却又需要被打包出来的 chunk 文件的名称。一般来说,这个 chunk 文件指的就是要懒加载的代码。
比如说我们业务代码中写了一份懒加载 lodash 的代码:
// 文件:index.js
// 创建一个 button
let btn = document.createElement("button");
btn.innerHTML = "click me";
document.body.appendChild(btn);
// 异步加载代码
async function getAsyncComponent() {
var element = document.createElement('div');
const { default: _ } = await import('lodash');
element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
return element;
}
// 点击 button 时,懒加载 lodash,在网页上显示 Hello! dynamic imports async
btn.addEventListener('click', () => {
getAsyncComponent().then(component => {
document.body.appendChild(component);
})
})
我们的 webpack 不做任何配置,还是原来的配置代码:
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
}
}
这时候的打包结果如下:
这个 1.min.js 就是异步加载的 chunk 文件。文档[4]里这么解释:
“
output.chunkFilename 默认使用 [id].js 或从 output.filename 中推断出的值([name] 会被预先替换为 [id] 或 [id].)
文档写得太抽象,我们不如结合上面的例子来看:
output.filename 的输出文件名是 [name].min.js,[name] 根据 entry 的配置推断为 index,所以输出为 index.min.js;
由于 output.chunkFilename 没有显示指定,就会把 [name] 替换为 chunk 文件的 id号,这里文件的 id 号是 1,所以文件名就是 1.min.js。
如果我们显式配置 chunkFilename,就会按配置的名字生成文件:
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
chunkFilename: 'bundle.js', // bundle.js
}
}
filename 指列在 entry 中,打包后输出的文件的名称。
chunkFilename 指未列在 entry 中,却又需要被打包出来的文件的名称。
这几个名词其实都是 webpack 魔法注释(magic comments)[5]里的,文档中说了 6 个配置,配置都可以组合起来用。我们说说最常用的三个配置。
前面举了个异步加载 lodash 的例子,我们最后把 output.chunkFilename 写死成 bundle.js。在我们的业务代码中,不可能只异步加载一个文件,所以写死肯定是不行的,但是写成 [name].bundle.js 时,打包的文件又是意义不明、辨识度不高的 chunk id。
{
entry: {
index: "../src/index.js"
},
output: {
filename: "[name].min.js", // index.min.js
chunkFilename: '[name].bundle.js', // 1.bundle.js,chunk id 为 1,辨识度不高
}
}
这时候 webpackChunkName 就可以派上用场了。我们可以在 import 文件时,在 import里以注释的形式为 chunk 文件取别名:
async function getAsyncComponent() {
var element = document.createElement('div');
// 在 import 的括号里 加注释 /* webpackChunkName: "lodash" */ ,为引入的文件取别名
const { default: _ } = await import(/* webpackChunkName: "lodash" */ 'lodash');
element.innerHTML = _.join(['Hello!', 'dynamic', 'imports', 'async'], ' ');
return element;
}
这时候打包生成的文件是这样的:
现在问题来了,lodash 是我们取的名字,按道理来说应该生成 lodash.bundle.js 啊,前面的 vendors~ 是什么玩意?
其实 webpack 懒加载是用内置的一个插件 SplitChunksPlugin[6] 实现的,这个插件里面有些默认配置项[7],比如说 automaticNameDelimiter,默认的分割符就是 ~,所以最后的文件名才会出现这个符号,这块儿内容我就不引申了,感兴趣的同学可以自己研究一下。
这两个配置一个叫预拉取(Prefetch),一个叫预加载(Preload),两者有些细微的不同,我们先说说 webpackPreload。
在上面的懒加载代码里,我们是点击按钮时,才会触发异步加载 lodash 的动作,这时候会动态的生成一个 script 标签,加载到 head 头里:
如果我们 import 的时候添加 webpackPrefetch:
...
const { default: _ } = await import(/* webpackChunkName: "lodash" */ /* webpackPrefetch: true */ 'lodash');
...
就会以 <link rel="prefetch" as="script"> 的形式预拉取 lodash 代码:
这个异步加载的代码不需要手动点击 button 触发,webpack 会在父 chunk 完成加载后,闲时加载 lodash 文件。
webpackPreload 是预加载当前导航下可能需要资源,他和 webpackPrefetch 的主要区别是:
webpackChunkName 是为预加载的文件取别名,webpackPrefetch 会在浏览器闲置下载文件,webpackPreload 会在父 chunk 加载时并行下载文件。
首先来个背景介绍,哈希一般是结合 CDN 缓存来使用的。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的 HTML 引用的 URL 地址也会改变,触发 CDN 服务器从源服务器上拉取对应数据,进而更新本地缓存。
hash 计算是跟整个项目的构建相关,我们做一个简单的 demo。
沿用案例 1 的 demo 代码,文件目录如下:
src/
├── index.css
├── index.html
├── index.js
└── utils.js
webpack 的核心配置如下(省略了一些 module 配置信息):
{
entry: {
index: "../src/index.js",
utils: '../src/utils.js',
},
output: {
filename: "[name].[hash].js", // 改为 hash
},
......
plugins: [
new MiniCssExtractPlugin({
filename: 'index.[hash].css' // 改为 hash
}),
]
}
生成的文件名如下:
我们可以发现,生成文件的 hash 和项目的构建 hash 都是一模一样的。
因为 hash 是项目构建的哈希值,项目中如果有些变动,hash 一定会变,比如说我改动了 utils.js 的代码,index.js 里的代码虽然没有改变,但是大家都是用的同一份 hash。hash 一变,缓存一定失效了,这样子是没办法实现 CDN 和浏览器缓存的。
chunkhash 就是解决这个问题的,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的 chunk,生成对应的哈希值。
我们再举个例子,我们对 utils.js 里文件进行改动:
export function square(x) {
return x * x;
}
// 增加 cube() 求立方函数
export function cube(x) {
return x * x * x;
}
然后把 webpack 里的所有 hash 改为 chunkhash:
{
entry: {
index: "../src/index.js",
utils: '../src/utils.js',
},
output: {
filename: "[name].[chunkhash].js", // 改为 chunkhash
},
......
plugins: [
new MiniCssExtractPlugin({
filename: 'index.[chunkhash].css' // // 改为 chunkhash
}),
]
}
构建结果如下:
我们可以看出,chunk 0 的 hash 都是一样的,chunk 1 的 hash 和上面的不一样。
假设我又把 utils.js 里的 cube() 函数去掉,再打包:
对比可以发现,只有 chunk 1 的 hash 发生变化,chunk 0 的 hash 还是原来的。
我们更近一步,index.js 和 index.css 同为一个 chunk,如果 index.js 内容发生变化,但是 index.css 没有变化,打包后他们的 hash 都发生变化,这对 css 文件来说是一种浪费。如何解决这个问题呢?
contenthash 将根据资源内容创建出唯一 hash,也就是说文件内容不变,hash 就不变。
我们修改一下 webpack 的配置:
{
entry: {
index: "../src/index.js",
utils: '../src/utils.js',
},
output: {
filename: "[name].[chunkhash].js",
},
......
plugins: [
new MiniCssExtractPlugin({
filename: 'index.[contenthash].css' // 这里改为 contenthash
}),
]
}
我们对 index.js 文件做了 3 次修改(就是改了改 log 函数的输出内容,过于简单就先不写了),然后分别构建,结果截图如下:
我们可以发现,css 文件的 hash 都没有发生改变。
hash 计算与整个项目的构建相关;
chunkhash 计算与同一 chunk 内容相关;
contenthash 计算与文件内容本身相关。
sourse-map ,里面都有个 map 了,肯定是映射的意思。sourse-map 就是一份源码和转换后代码的映射文件。具体的原理内容较多,感兴趣的同学可以自行搜索,我这里就不多言了。
我们先从官网上看看 sourse-map 有多少种类型:
emmmm,13 种,告辞。
如果再仔细看一下,就发现这 13 种大部分都是 eval、cheap、inline 和 module这 4 个词排列组合的,我做了个简单的表格,比官网上直白多了:
参数参数解释eval打包后的模块都使用 eval() 执行,行映射可能不准;不产生独立的 map 文件cheapmap 映射只显示行不显示列,忽略源自 loader 的 source mapinline映射文件以 base64 格式编码,加载 bundle 文件最后,不产生独立的 map 文件module增加对 loader source map 和第三方模块的映射
还不明白?可以看看 demo。
我们对 webpack 做一些配置,devtool 是专门配置 source-map 的。
......
{
devtool: 'source-map',
}
......
index.js 文件为了简便,我们只写一行代码,为了得出报错信息,我们故意拼错:
console.lg('hello source-map !') // log 写成 lg
下面我们试一试常见的几个配置:
source-map 是最大而全的,会生成独立 map 文件:
注意下图光标的位置,source-map 会显示报错的行列信息:
cheap,就是廉价的意思,它不会产生列映射,相应的体积会小很多,我们和 sourse-map 的打包结果比一下,只有原来的 1/4 。
eval-source-map 会以 eval() 函数打包运行模块,不产生独立的 map 文件,会显示报错的行列信息:
// index.bundle.js 文件
!function(e) {
// ......
// 省略不重要的代码
// ......
}([function(module, exports) {
eval("console.lg('hello source-map !');//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi4vc3JjL2luZGV4Mi5qcz9mNmJjIl0sIm5hbWVzIjpbImNvbnNvbGUiLCJsZyJdLCJtYXBwaW5ncyI6IkFBQUFBLE9BQU8sQ0FBQ0MsRUFBUixDQUFXLG9CQUFYIiwiZmlsZSI6IjAuanMiLCJzb3VyY2VzQ29udGVudCI6WyJjb25zb2xlLmxnKCdoZWxsbyBzb3VyY2UtbWFwICEnKSJdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///0\n")
}
]);
映射文件以 base64 格式编码,加在 bundle 文件最后,不产生独立的 map 文件。加入 map 文件后,我们可以明显地看到包体积变大了;
// index.bundle.js 文件
!function(e) {
}([function(e, t) {
console.lg("hello source-map !")
}
]);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4uL3NyYy9pbmRleDIuanMiXSwibmFtZXMiOlsiaW5zdGFsbGVkTW9kdWxlcyIsIl9fd2VicGFja19yZXF1aXJ......
// base64 太长了,我删了一部分,领会精神
上面的几个例子都是演示,结合[官网推荐](https://webpack.docschina.org/configuration/devtool/#%E5%AF%B9%E4%BA%8E%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83[8])和实际经验,常用的配置其实是这几个:
1.source-map
大而全,啥都有,就因为啥都有可能会让 webpack 构建时间变长,看情况使用。
2.cheap-module-eval-source-map
这个一般是开发环境(dev)推荐使用,在构建速度报错提醒上做了比较好的均衡。
3.cheap-module-source-map
一般来说,生产环境是不配 source-map 的,如果想捕捉线上的代码报错,我们可以用这个
般地,URL和URI比较难以区分。接下来,本文以区分URL和URI为引子,详细介绍URL的用法,JavaScript重文·也许你该知道浏览器输入 URL 后发生了什么?
URI是Uniform Resource Identifier的缩写,称为统一资源标识符。URI是一个通用的概念,由两个主要的子集URL和URN构成,URL是通过描述资源的位置来标识资源的,而URN则是通过名字来识别资源的,与它们当前所处位置无关
URL是Uniform Resource Locator的缩写,称为统一资源定位符。URL正是使用web浏览器等访问web页面时需要输入的网页地址
URL是一种强有力的工具。但URL并不完美。它表示的是实际的地址,而不是准确的名字。这种方案的缺点在于如果资源被移走了,URL也就不再有效了。那时,它就无法对对象进行定位了
如果有了对象的准确名称,则不论其位于何处都可以找到这个对象。就像人一样,只要给定了资源的名称和其他一些情况,无论资源移到何处,都能够追踪到它。为了应对这个问题,因特网工程任务组(Internet Engineering Task Force, IETF) 已经对URN的新标准做了一段时间的研究了。无论对象搬移到什么地方,URN都能为对象提供一个稳定的名称
但是,从URL转换成URN是一项巨大的工程,支持URN需要进行很多改动——标准主体的一致性,对各种HTTP应用程序的修改等。所以,还要等待更合适的时机才能进行这种转换
URL语法建立在由下面9部分构成的通用格式上。其中,URL最重要的3个部分是方案(scheme)、主机(host)和路径(path)
<scheme>://<user>:<password>@<host>:<port>/<path>:<params>?<query>#<frag>
【方案】
方案实际上是规定如何访问指定资源的主要标识符,它会告诉负责解析URL的应用程序应该使用什么协议
方案组件必须以一个字母符号开始,由第一个“:”符号将其与URL的其余部分分隔开来。方案名是大小写无关的,因此URL“http://www.hardware.com”和“HTTP://www.hardware.com” 是等价的
常见的方案如下
1、HTTP
HTTP是一种超文本传输协议方案,除了没有用户名和密码之外,与通用的URL格式相符。如果省略了端口,就默认为80
基本格式:
http://<host>:<port>/<path>?<query>#<frag>
示例:
http://www.hardware.com/index.html http://www.hardware.com:80/index.html
2、https
方案https与方案http是一对。唯一的区别在于方案https使用了网景的SSL, SSL为HTTP连接提供了端到端的加密机制。其语法与HTTP的语法相同,默认端口为443
基本格式:
https://<host>:<port>/<path>?<query>#<frag>
示例:
https://www.hardware.com/secure.html
3、Mailto
Mailto URL指向的是E-mail地址。由于E-mail的行为与其他方案都有所不同(它并不指向任何可以直接访问的对象),所以mailto URL的格式与标准URL的格式也有所不同
示例:
mailto:joe@hardware.com
4、ftp
文件传输协议URL可以用来从FTP服务器上下载或向其上传文件,并获取FTP服务器上的目录结构内容的列表
在Web和URL出现之前,FTP就已经存在了。Web应用程序将FTP作为一种数据访问方案使用
基本格式:
ftp://<user>:<password>@<host>:<port>/<path>;<params>
示例:
ftp://anonymous:joe%40hardware.com@prep.ai.mit.edu:21/pub/gnu/
5、rtsp,rtspu
RTSP URL是可以通过实时流传输协议(Real Time Streaming Protocol)解析的音/视频媒体资源的标识符
方案rtspu中的u表示它是使用UDP协议来获取资源的
基本格式:
rtsp://<user>:<password>@<host>:<port>/<path> rtspu://<user>:<password>@<host>:<port>/<path>
示例:
rtsp://www.hardware.com:554/interview/cto_video
6、file
方案file表示一台指定主机(通过本地磁盘、网络文件系统或其他一些文件共享系统)上可直接访问的文件。各字段都遵循通用格式。如果省略了主机名,就默认为正在使用URL的本地主机
基本格式:
file ://<host>/<path>
示例:
file://OFFICE-FS/policies/casual-fridays.doc
7、telnet
方案telnet用于访问交互式业务。它表示的不是对象自身,而是可通过telnet协议访问的交互式应用程序(资源)
基本格式:
telnet://<user>:<password>@<host>:<port>/
示例:
telnet://slurp:webhound@joes-hardware.com:23/
[注意]除了以上常见的7种方案之外,如果要查看全部的URI方案列表,请移步至https://www.w3.org/Addressing/schemes.html
【主机和端口】
要想在因特网上找到资源,应用程序要知道是哪台机器装载了资源,以及在那台机器的什么地方可以找到能对目标资源进行访问的服务器。URL的主机和端口组件提供了这两组信息
主机组件标识了因特网上能够访问资源的宿主机器。可以用上述主机名(www.hardware.com)或者IP地址来表示主机名
[注意]IP地址可以是192.168.1.1这类IPv4地址名,还可以是[0:0:0:0:0:0:0:1]这样用括号括起来的IPv6地址名
比如,下面两个URL就指向同一个资源——第一个URL通过主机名,第二个通过IP地址指向服务器:
http://www.hardware.com:80/index.html http://161.58.228.45:80/index.html
端口组件标识了服务器正在监听的网络端口。对下层使用了TCP协议的HTTP来说,默认端口号为80
【用户名和密码】
很多服务器都要求输入用户名和密码才会允许用户访问数据。FTP服务器就是这样一个常见的实例
ftp://ftp.prep.ai.mit.edu/pub/gnu ftp://anonymous@ftp.prep.ai.mit.edu/pub/gnu ftp://anonymous:my_passwd@ftp.prep.ai.mit.edu/pub/gnu http://joe:joespasswd@www.joes-hardware.com/sales_info.txt
第一个例子没有用户或密码组件,只有标准的方案、主机和路径。如果某应用程序使用的URL方案要求输入用户名和密码,比如FTP,但用户没有提供,它通常会插入一个默认的用户名和密码。比如,如果向浏览器提供一个FTP URL,但没有指定用户名和密码,它就会插入anonymous(匿名用户)作为你的用户名,并发送一个默认的密码(IE会发送IEUser)
第二个例子显示了一个指定为anonymous的用户名。这个用户名与主机组件组合在一起,看起来就像E-mail地址一样。字符将用户和密码组件与URL的其余部分分隔开来
在第三个例子中,指定了用户名(anonymous)和密码(my_passwd),两者之间由字符“:”分隔
【路径】
URL的路径组件说明了资源位于服务器的什么地方。路径通常很像一个分级的文件系统路径
http://www.hardware.com:80/seasonal/index-fall.html
这个URL中的路径为/seasonal/index-fall.html,很像UNIX文件系统中的文件系统路径。路径是服务器定位资源时所需的信息。可以用字符“/”将HTTP URL的路径组件划分成一些路径段(path segment),每个路径段都有自己的参数(param)组件
【参数】
对很多方案来说,只有简单的主机名和到达对象的路径是不够的。除了服务器正在监听的端口,以及是否能够通过用户名和密码访问资源外,很多协议都还需要更多的信息才能工作
负责解析URL的应用程序需要这些协议参数来访问资源。否则,另一端的服务器可能就不会为请求提供服务,或者更糟糕的是,提供错误的服务。比如,像FTP这样的协议,有两种传输模式:二进制和文本形式。肯定不希望以文本形式来传送二进制图片,这样的话,二进制图片可能会变得一团糟
为了向应用程序提供它们所需的输入参数,以便正确地与服务器进行交互,URL中有一个参数组件。这个组件就是URL中的名值对列表,由字符“;”将其与URL的其余部分(以及各名值对)分隔开来。它们为应用程序提供了访问资源所需的所有附加信息。比如:
ftp://prep.ai.mit.edu/pub/gnu;type=d
在这个例子中,有一个参数type=d,参数名为type,值为d
如前所述,HTTP URL的路径组件可以分成若干路径段。每段都可以有自己的参数。比如:
http://www.hardware.com/hammers;sale=false/index.html;graphics=true
这个例子就有两个路径段,hammers和index.html。hammers路径段有参数sale,其值为false。index.html段有参数graphics,其值为true
【查询字符串】
很多资源,比如数据库服务,都是可以通过提问题或进行査询来缩小所请求资源类型范围的。假设数据库中维护着一个未售货物的清单,并可以对淸单进行査询,以判断产品是否有货,那就可以用下列URL来査询Web数据库网关,看看编号为12731、颜色为blue、尺寸为large的条目是否有货:
http://www.hardware.com/inventory-check.cgi?item=12731&color=blue&size=large
这个URL的大部分都与我们见过的其他URL类似。只有问号(?)右边的内容是新出现的。这部分被称为查询(query)组件。URL的査询组件和标识网关资源的URL路径组件一起被发送给网关资源
除了有些不合规则的字符需要特别处理之外,对査询组件的格式没什么要求。按照常规,很多网关都希望査询字符串以一系列“名/值”对的形式出现,名值对之间用字符“&”分隔
【片段】
有些资源类型,比如HTML,除了资源级之外,还可以做进一步的划分。比如,对一个带有章节的大型文本文档来说,资源的URL会指向整个文本文档,但理想的情况是,能够指定资源中的那些章节
为了引用部分资源或资源的一个片段,URL支持使用片段(frag)组件来表示一个资源内部的片段。比如,URL可以指向HTML文档中一个特定的图片或小节
片段挂在URL的右手边,最前面有一个字符“#”。比如:
http://www.hardware.com/tools.html#drills
在这个例子中,片段drills引用了Web服务器上页面/tools.html中的一个部分。这部分的名字叫做drills
HTTP服务器通常只处理整个对象,而不是对象的片段,客户端不能将片段传送给服务器。浏览器从服务器获得了整个资源之后,会根据片段来显示感兴趣的那部分资源
字符
URL的设计者们认识到有时人们可能会希望URL中包含除通用的安全字母表之外的二进制数据或字符。因此,需要有一种转义机制,能够将不安全的字符编码为安全字符,再进行传输
人们设计了一种编码机制,用来在URL中表示各种不安全的字符。这种编码机制就是通过一种“转义”表示法来表示不安全字符的,这种转义表示法包含一个百分号(%),后面跟着两个表示字符ASCII码的十六进制数
下面是一些例子
在URL中,有几个字符被保留起来,有着特殊的含义。有些字符不在定义的US- ASCII可打印字符集中。还有些字符会与某些因特网网关和协议产生混淆,因此不赞成使用
下面列出了保留及受限的字符
【encodeURI()】
encodeURI()函数把字符串作为URI进行编码,实际上enchodeURI()函数只把参数中的空格编码为%20,其余特殊字符均不会转换
encodeURI()的不编码字符有82个:
! # $ & ' ( ) * + , - . / : ;=? @ _ ~ 0-9 a-z A-Z
使用方式
//'http://www.w3school.com.cn<br />' console.log(encodeURI("http://www.w3school.com.cn")+ "<br />") //'http://www.w3school.com.cn/My%20first/' console.log(encodeURI("http://www.w3school.com.cn/My first/")) //',/?:@&=+$#' console.log(encodeURI(",/?:@&=+$#"))
[注意]encodeURI()可以编码中文
//'%E6%B5%8B%E8%AF%95' console.log(encodeURI('测试'));
【decodeURI()】
decodeURI()函数可对encodeURI()函数编码过的URI进行解码。实际上,decodeURI()仅仅会把%20转换为空格显示
//"http://www.w3school.com.cn/My first/" console.log(decodeURI("http://www.w3school.com.cn/My%20first/"));
【encodeURIComponent()】
encodeURIComponent()函数可把字符串作为URI组件进行编码。该方法主要对;/?:@&=+$,#等这些用于分隔URI组件的字符以及中文进行编码
encodeURIComponent不编码字符有71个:
!, ',(,),*,-,.,_,~,0-9,a-z,A-Z
由于此方法对:/都进行了编码,所以不能用它来对网址进行编码,而适合对URI中的参数进行编码
[注意]encodeURIComponent()可以编码中文
var uri="http://www.wrox.com/illegal value.htm#start"; //'http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start' console.log(encodeURIComponent(uri)); //'%E6%B5%8B%E8%AF%95' console.log(encodeURIComponent('测试'));
【decodeURIComponent()】
decodeURIComponent()函数可对encodeURIComponent()函数编码的URI进行解码
var uri='http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start'; //'http://www.wrox.com/illegal value.htm#start' console.log(decodeURIComponent(uri));
【escape()】
escape()函数对字符串进行编码,将字符的unicode编码转化为16进制序列
ES3中反对escape()的使用,并建议用encodeURI和encodeURIComponent代替,不过escape()依然被广泛的用于cookie的编码,因为escape()恰好编码了cookie中的非法字符并且对路径中常出现的“/”不进行编码
escape()的不编码字符有69个:
* + - . / @ _ 0-9 a-z A-Z
[注意]escape()可以编码中文
var uri="http://www.wrox.com/illegal value.htm#start"; //'http%3A//www.wrox.com/illegal%20value.htm%23start' console.log(escape(uri)); //%u6D4B%u8BD5 console.log(escape('测试'));
【unescape()】
unescape()函数用于对escape()函数编码的URI进行解码
//http://www.wrox.com/illegal value.htm#start console.log(unescape('http%3A//www.wrox.com/illegal%20value.htm%23start')); //'测试' console.log(unescape('%u6D4B%u8BD5'));
链接文章:
https://www.cnblogs.com/xiaohuochai/p/6144157.html
https://www.cnblogs.com/xiaohuochai/p/6144157.html
https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/What_is_a_URL
JS文件添加右键菜单,一键完成JS混淆加密
将“JS混淆加密”集成到鼠标右键菜单
Windows一键JS混淆加密:功能集成到鼠标右键菜单
目标:将“JS混淆加密”功能集成到鼠标右键菜单,一键点击完成JavaScript代码混淆加密。
原因:为什么实现这个功能,有什么好处?
答:方便、易用。JavaScript程序员经常需要将代码进行混淆加密。
操作过程,通常情况下,需要将代码复制或提交到网站或软件中,以使用JShaman为例,需要复制代码到JShaman网站,完成JS混淆加密,再把代码粘贴回来。
JShaman网站不需要注册、不需要登录,打开网站、复制粘贴就可使用。虽然已经很方便,但集成到鼠标右键菜单可以更方便。
效果展示:
实现方法:
环境需求:Node.JS
1、NodeJS脚本文件准备
实现此功能,需要用NodeJS运行一个脚本JS文件,以便调用JShaman WebApi接口,进行JavaScript混淆加密。
JS代码如下,将此文件保存为obfuscate.js,放于任一目录下:
/**
* 调用JShaman.com WebAPI接口 实现JavaScript混淆加密
*/
/**
* 配置部分
*/
//JShaman.com VIP码,免费使用设为"free",如已购买VIP码,在此修改
const vip_code="free";
//混淆加密参数配置,免费使用时无需配置
//参数详细说明请参见JShaman官网,设为"true"启用功能、设为"false"不启用
const config={
//压缩代码
compact: "true",
//混淆全局变量名和函数名
renameGlobalFunctionVariable: "false",
//平展控制流
controlFlowFlattening: "true",
//僵尸代码植入
deadCodeInjection: "false",
//字符串阵列化
stringArray: "true",
//阵列字符串加密
stringArrayEncoding: "false",
//禁用命令行输出
disableConsoleOutput: "false",
//反浏览器调试
debugProtection: "false",
//时间限定
time_range: "false",
//时间限定起始时间、结束时间,时间限定启用时此2参数生效
time_start: "20240118",
time_end: "20240118",
//域名锁定
domainLock: [],
//保留关键字
reservedNames: [],
}
/**
* 以下代码实现向JShaman.com WebApi发送请求完成JavaScript混淆加密,无需修改
*/
const fs=require("fs");
const readline=require("readline");
const request=require("request");
//获取命令行参数中的文件路径
//获取右键菜单调用的文件路径
const filePath=process.argv[2];
if(!filePath) {
console.error("未选中文件");
process.exit(1);
}
//确保文件存在
if(!fs.existsSync(filePath)) {
console.error(`文件 ${filePath} 不存在`);
process.exit(1);
}
console.log(`正在处理文件:${filePath}`);
//从文件中获取JavaScript代码
var javascript_code=fs.readFileSync(filePath,"utf8").toString();
//Post请求配置
var options={
url: "https://www.jshaman.com:4430/submit_js_code/",
method: "POST",
json: true,
body: {
//JavaScript代码
"js_code": javascript_code,
//JShaman VIP码
"vip_code": vip_code,
}
};
//使用free为VIP码、免费调用JShaman WebAPI接口时,不能配置参数,仅可实现较低强度代码保护
//如果购买了JShaman的VIP码,则可启用配置,实现高强度JavaScript混淆加密
if(options.body.vip_code !="free"){
//混淆加密参数
options.body.config=config;
}
console.log("正在向JShaman.com提交混淆加密请求...")
//发送请求到JShaman服务器,进行JavaScript混淆加密
request(options, function(error, response, body) {
if (!error && response.statusCode===200) {
//输出返回消息
console.log(body.message);
//返回状态值为0是成功标识
if(body.status==0){
const rl=readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question("是否覆盖原文件?y为是,n则在同目录下另存。(y/n) ", (answer)=> {
if (answer.toLowerCase()==="y") {
//输入y,覆盖原文件
fs.writeFileSync(filePath, body.content.toString());
console.log("文件已覆盖。");
} else {
var obfuscated_file=filePath + ".obfuscated.js";
fs.writeFileSync(obfuscated_file, body.content.toString());
console.log("混淆加密后的Js文件:", obfuscated_file);
}
rl.close();
});
}
} else {
console.error("向JShaman.com发送请求时出现错误:", error);
}
});
//按下任意键退出程序
process.on("beforeExit", (code)=> {
const rl=readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("按下任意键退出程序...");
rl.on("line", (input)=> {
rl.close();
process.exit(0);
});
});
说明:此JS脚本默认调用JShaman提供的免费Web API接口。JShaman提供有免费、商业两种接口。如您已获得JShaman VIP 码,可修改上述代码中的VIP码、使用商业接口,能使配置参数生效、获得更好的JS混淆加密效果。
2、注册表修改
修改注册表是为给JS文件添加右键菜单,以便在右键点击.js文件时菜单中显示“混淆加密”功能。
将下面的代码保存为right_click_menu.reg:
Windows Registry Editor Version 5.00
; 为 .js 文件类型定义新的上下文菜单项
[HKEY_CLASSES_ROOT\SystemFileAssociations\.js\Shell\JavaScript Obfuscate]
"MuiVerb"="@shell32.dll,-30525"
"Icon"="imageres.dll,-102"
"Position"="Bottom"
; 定义点击菜单项时要执行的命令
[HKEY_CLASSES_ROOT\SystemFileAssociations\.js\Shell\JavaScript Obfuscate\command]
@="\"C:\\Program Files\\nodejs12\\node.exe\" \"C:\\Users\\WangLiwen\\Desktop\\JShaman_JavaScript_Obfuscator\\obfuscate.js\" \"%1\""
说明:“JavaScript Obfuscate”是菜单中的显示的文字内容,可修改。
保存之后,双击将内容导入注册表:
仅此两步,已经大功告成,可以使用了。
JS文件混淆加密测试:
测试,加密一个JS文件,如上图所示。
使用感受:
如此进行“JS文件混淆加密”,非常方便、又快又好!
*请认真填写需求信息,我们会在24小时内与您取得联系。