webpack bootstrap源码解读

栏目: CSS · CSS3 · 发布时间: 6年前

内容简介:虽然一直在用webpack,但很少去看它编译出来的js代码,大概是因为调试的时候有sourcemap,可以直接调试源码。一时心血来潮想研究一下,看了一些关于webpack编译方面的文章都有提到,再结合自己看源码的体会,记录一下自己的理解说bootstap可能还有点不好理解,看一下webpack编译出来的js文件就很好理解了:编译后的文件跟我们的源文件不太一样了,原本的内容被放到了一个

虽然一直在用webpack,但很少去看它编译出来的js代码,大概是因为调试的时候有sourcemap,可以直接调试源码。一时心血来潮想研究一下,看了一些关于webpack编译方面的文章都有提到,再结合自己看源码的体会,记录一下自己的理解

说bootstap可能还有点不好理解,看一下webpack编译出来的js文件就很好理解了:

// 编译前的入口文件index.js的内容
let a = 1;
console.log(a);

// webpack编译后的文件内容
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {


let a = 1;
console.log(a);

/***/ })
],[0]);

编译后的文件跟我们的源文件不太一样了,原本的内容被放到了一个 function(module, exports){} 函数里,而最外层多了一个 webpackJsonp 的执行代码。那么问题来了:

function(module, exports){}

这就是bootstrap的作用了。如果不用code split把bootstrap单独分离出来,它就在编译出的js文件最上面,因为需要先执行bootstrap后续的代码才能执行。我们可以用 CommonChunkPlugin 把它单独提出来,方便我们阅读。把下面的代码写到你的webpack的 plugin配置 里即可:

new webpack.optimize.CommonsChunkPlugin({
    name: "manifest" // 可以叫manifest,也可以用runtime
}),

配置之后,编译出来的文件会多出一个 manifest.js 文件,这就是webpack bootstrap的代码了。bootstrap和用户代码(就是我们自己写的部分)编译后的文件其实是一个整体,所以后面的分析会引入用户代码一起看

manifest.js

manifest源码分为3个部分:

  1. 创建了一个闭包,初始化需要用到的变量
  2. 定义webpackJsonp方法,挂载到window变量下
  3. 定义与编译相关的辅助函数和变量,如 __webpack_require__ (也就是我们在自己的代码里用到的 require 语法)

我们一个一个来看。下面的每个部分,我们都只截取manifest源码的相关部分来看,完整的源码放在文章最后了

初始化部分

/******/ (function(modules) { // webpackBootstrap
            // ......

            // The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// objects to store loaded and loading chunks
/******/ 	var installedChunks = {
/******/ 		1: 0
/******/ 	};

            // ......
/******/ })
/************************************************************************/
/******/ ([]);

我们截取了manifest最外层的代码和初始化部分的代码,可以看到整个文件都被一个闭包括在里面,而 modules 的初始值是一个空的 Array ( [] )。 这样做可以隔离作用域,保护内部的变量不被污染

  • modules 空的 Array ( [] ),用来存放每个module的内容
  • installedModules 存放module的cache,一个module被执行后(module的执行会在webpackJsonp的源码部分提到)的结果被保存到这里,之后再用到这个模块就可以直接使用缓存而无需再次执行了
  • installedChunks 用来存放chunk的执行情况。若一个chunk已经加载了,在installedChunks里这个chunk的值会变成0,也就是无需再加载了

如果分不清module和chunk这两个概念的区别,文章最后一节专门对此作了解释

webpackJsonp

源码分析

在讲webpackJsonp的源码之前,先回忆一下我们自己的chunk代码

// 编译前的入口文件index.js的内容
let a = 1;
console.log(a);

// webpack编译后的文件内容
webpackJsonp([0],[
/* 0 */
/***/ (function(module, exports) {


let a = 1;
console.log(a);

/***/ })
],[0]);

执行webpackJsonp,传了3个参数:

  • chunkIds chunk的id,这里用了array,但一般一个文件就是一个chunk

  • moreModules chunk里所有模块的内容。模块内容可能不是很直观,再看上面编译后的代码,我们的代码被包在 function(module, exports) {} 里,其实是变成了一个函数,这就是一个模块内容。这其实是CommonJs规范中一个模块的定义,只是我们在写模块的时候不用自己写这个头尾,工具会帮我们生成。还记得AMD规范吗?

    moreModules还隐藏了对每个module的id的定义。从编译后的文件里可以看到 /* 0 */ 这样的注释,结合代码来看,其实module的id就是它在moreModules里的数组下标。那么问题来了,只有一个entry chunk还好说,如果有多个chunk,每个chunk里的moreModules的Id不会冲突吗?这里有个小技巧,如下是一个异步chunk的部分代码:

    webpackJsonp([0],[
    /* 0 */,
    /* 1 */,
    /* 2 */,
    /* 3 */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    // ......

    看到了吗,moreModules的前3个元素是空的,也就是说 0-2 这三个id已经被别的chunk使用了

  • executeModules 需要执行的module,也是一个array。并不是每一个chunk都有executeModules,事实上只有entry chunk才有,因为entry.js是需要执行的

ok,有了使用webpackJsonp部分的印象,再来看webpackJsonp代码会清晰很多

/******/ 	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/ 		// add "moreModules" to the modules object,
/******/ 		// then flag all "chunkIds" as loaded and fire callback
/******/ 		var moduleId, chunkId, i = 0, resolves = [], result;
                // 
/******/ 		for(;i < chunkIds.length; i++) {       // part 1
/******/ 			chunkId = chunkIds[i];
/******/ 			if(installedChunks[chunkId]) {
/******/ 				resolves.push(installedChunks[chunkId][0]);
/******/ 			}
/******/ 			installedChunks[chunkId] = 0;
/******/ 		}
                // 取出每个module的内容
/******/ 		for(moduleId in moreModules) {         // part 2
/******/ 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ 				modules[moduleId] = moreModules[moduleId];
/******/ 			}
/******/ 		}
                // 
/******/ 		if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/ 		while(resolves.length) {                // part 3
/******/ 			resolves.shift()();
/******/ 		}
                // 执行executeModules
/******/ 		if(executeModules) {                    // part 4
/******/ 			for(i=0; i < executeModules.length; i++) {
/******/ 				result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ 			}
/******/ 		}
/******/ 		return result;
/******/ 	};

首先,webpackJsonp是挂在 window 全局变量上的,看看每个chunk的开头就知道为什么。我把它分为4块:

  • part 1 这部分涉及到 installedChunks ,我们之前了解过,如果没有 异步加载的chunk ,这部分是用不到的,我们留到异步chunk再说

  • part 2 取出这个chunk里所有module的内容,放到 modules 里,这里并不执行每个module,而是真正用到这个module时再从modules里取出来执行

  • part 3 与part 1一样是对 installedChunks 的操作,放到后面再说

  • part 4 执行executeModules,一般只有入口文件对应的module是需要执行的。执行module调用了 __webpack_require__ 方法。

    还记得我们在代码里怎么引入别的js吗? 对, require 方法。其实我们的代码编译后会被转成 __webpack_require__ ,只不过要把引用的路径换成moduleId,这一步也是webpack处理的。所以 __webpack_require__ 的作用就是执行一个module,把它的 exports 返回。先来看看它的实现:

    // The require function
    /******/ 	function __webpack_require__(moduleId) {
    /******/
    /******/ 		// Check if module is in cache
    /******/ 		if(installedModules[moduleId]) {   // line 1
    /******/ 			return installedModules[moduleId].exports;
    /******/ 		}
    /******/ 		// Create a new module (and put it into the cache)
    /******/ 		var module = installedModules[moduleId] = {  // line 2
    /******/ 			i: moduleId,
    /******/ 			l: false,
    /******/ 			exports: {}
    /******/ 		};
    /******/
    /******/ 		// Execute the module function
    /******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // line 3
    /******/
    /******/ 		// Flag the module as loaded
    /******/ 		module.l = true;
    /******/
    /******/ 		// Return the exports of the module
    /******/ 		return module.exports;  // line 4
    /******/ 	}

    line 1 检查这个module是不是已经执行过,是的话一定在缓存 installedModules 里,直接把缓存里的 exports 返回。如果没有执行过,那就新建一个module,也就是 line 2 。这里module有2个额外的属性, i 记录moduleId, l 记录module是否已经执行。

    line 3 执行这个module。我们前面说过,我们的代码都被包在一个函数里了,这个函数提供3个参数: module , exports , require 。仔细看这行,是不是这三个参数都被传进去了。

    line 4 返回 exports 。值得一提的是, line 3 的执行结果是传给了 line 2 我们新建的 module 变量,也就是把 exports 赋值给 module 了,所以我们直接返回了 module.exports

使用场景

webpackJsonp的使用场景跟chunk相关,有异步chunk的情况会复杂一些

没有异步加载chunk的情况

没有异步加载chunk的情况是很简单的,它的执行过程可以简单归纳为:依次执行每个chunk文件,也就是执行 webpackJsonp ,从 moreModules 里取出每个module的内容,放到 modules 里,然后执行入口文件对应的module。因为每次执行module,都会缓存这个module的执行结果,所以即使你没有抽取出每个chunk里的相同module(CommonChunkPlugin),也不会重复执行重复的module

有异步加载chunk的情况

当我们使用 require.ensure 或者 import() 语法时就会产生一个异步chunk,官方文档传送门。异步chunk的js文件不需要手动写到html里,在执行到它时会通过动态加载 script 的方式引入,异步加载的函数就是 __webpack_require__.e

// This file contains only the entry chunk.
/******/ 	// The chunk loading function for additional chunks
/******/ 	__webpack_require__.e = function requireEnsure(chunkId) {
/******/ 		var installedChunkData = installedChunks[chunkId];
/******/ 		if(installedChunkData === 0) {   // part 1
/******/ 			return new Promise(function(resolve) { resolve(); });
/******/ 		}
/******/
/******/ 		// a Promise means "currently loading".
/******/ 		if(installedChunkData) {    // part 2
/******/ 			return installedChunkData[2];
/******/ 		}
/******/
/******/ 		// setup Promise in chunk cache
/******/ 		var promise = new Promise(function(resolve, reject) { // part 3
/******/ 			installedChunkData = installedChunks[chunkId] = [resolve, reject]
/******/ 		});
/******/ 		installedChunkData[2] = promise;
/******/
/******/ 		// start chunk loading
/******/ 		var head = document.getElementsByTagName('head')[0];  // part 4
/******/ 		var script = document.createElement('script');
/******/ 		script.type = "text/javascript";
/******/ 		script.charset = 'utf-8';
/******/ 		script.async = true;
/******/ 		script.timeout = 120000;
/******/
/******/ 		if (__webpack_require__.nc) {
/******/ 			script.setAttribute("nonce", __webpack_require__.nc);
/******/ 		}
/******/ 		script.src = __webpack_require__.p + "" + ({"0":"modC","1":"modA"}[chunkId]||chunkId) + ".js"; // line 1
/******/ 		var timeout = setTimeout(onScriptComplete, 120000);
/******/ 		script.onerror = script.onload = onScriptComplete;
/******/ 		function onScriptComplete() { // line 2
/******/ 			// avoid mem leaks in IE.
/******/ 			script.onerror = script.onload = null;
/******/ 			clearTimeout(timeout);
/******/ 			var chunk = installedChunks[chunkId];
/******/ 			if(chunk !== 0) {
/******/ 				if(chunk) {
/******/ 					chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
/******/ 				}
/******/ 				installedChunks[chunkId] = undefined;
/******/ 			}
/******/ 		};
/******/ 		head.appendChild(script);
/******/
/******/ 		return promise;
/******/ 	};

代码有点多~但其实大部分(part 4)都是异步加载script。我们从头开始看

  • part 1 判断chunk是否已经加载过了,是的话直接返回一个空的Promise。为什么在 installedChunks 里的记录为 0 就表示已经加载过了?这要回到我们之前在讲 webpackJsonp 跳过的部分,单独截下来看:

    for(;i < chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]); // line 1
        }
        installedChunks[chunkId] = 0; // line 2
    }

    加载当前chunk时在 installedChunks 里记录这个chunk已经加载了,也就是 置0 了(line 1)

  • part 2part 3 是一体的,它的作用是在chunk还没加载好时就被使用了,这时先返回一个promise,等chunk加载好了,这个promise会 resolve ,通知调用者可以使用这个chunk了。因为chunk的js文件需要通过网络,不能保证什么时候加载好,才会用到promise。我们先看看是怎么实现的:

    其实应该倒过来先看 part 3 再看 part 2part 3 定义了一个promise, 然后把这个promise的 resolve 放到 installedChunks 里了 。这一步很关键,因为chunk加载时需要执行这个resolve告诉这个chunk的使用者已经可以使用了。 part 3 执行完成后, installedChunks 里这个chunk对应的记录应该是一个 Array 且有3个元素:这个promise的resolve,reject和promise本身。另外需要注意一点, new Promise(function(){}) 语句的 function 是立即执行的。

    再来看 part 2 ,如果 installedChunks 里有这条记录,且它又没有加载完成,那么就把 part 3 定义的promise返回给调用者。这样的作用是,当chunk加载完成了,只需要执行这个promise的 resolve 就能通知调用者继续往下执行

    顺带提一下这个promise的resolve是何时执行的。看 part 1 webpackJsonp的代码 line 1 这行, installedChunks[chunkId][0] 是不是很眼熟,对,这就是chunk在为加载完成时创建的promise的resolve方法,而后会把所有的使用到这个chunk的resolve方法都执行(如下),因为执行到 webpackJsonp 就说明这个chunk已经加载完成了

    while(resolves.length) {
        resolves.shift()();
    }
  • part 4 是动态加载 script 的代码,没什么可说的,值得一提的是 line 1 在拼接script的src时出现的 {"0":"modC","1":"modA"} ,这个是我自己的两个异步chunk的id,是webpack分析依赖后插入进来的,如果你有多个异步chunk,这里会随之变化。

    line 2 是异步chunk加载超时和报错时的处理

ok,有了 __webpack_require__.e 的理解,我们再来看加载异步chunk的情况就很轻松了。先来看一段示例:

// 编译前
import(/* webpackChunkName: "modA" */ './mods/a').then(a => {
    let ret = a();
    console.log('ret', ret);
})

// 编译后
__webpack_require__.e/* import() */(0).then(__webpack_require__.bind(null, 0)).then(a => {
    let ret = a();
    console.log('ret', ret);
})

我们用 import() 的方式做code spliting,换成 require.ensure 也类似,区别在 import() 的返回值是promise形式的, require.ensure 是callback形式。对比编译前后,import被替换成了 __webpack_require__.e ,在源码的 .then 中间加了一行 .then(__webpack_require__.bind(null, 0))

首先, __webpack_require__.e 保证chunk异步加载完成,但是并不返回chunk的执行结果(见上文__webpack_require__.e的源码分析),所以加了一个 .thenrequire 这个chunk里的module。再然后,就是我们取这个module的代码了

注: /* webpackChunkName: "modA" */ 这个是给chunk起名字的,webpack会读这段注释,取 modA 作为这个chunk的name,在 output.chunkFilename 可以用 [name].js 来命名这个chunk,不然webpack会用数字id作为chunk的文件名

其他辅助函数

webpack_require.p

等于 output.publicPath 的值(publicPath传送门)。webpack在编译时会把源码中的本地路径替换成publicPath的值,但是异步chunk是动态加载的,它的 src 需要加上publicPath。看个小栗子就明白了:

// webpack.config.js
module.exports = {
    entry: path.resolve("test", "src", "index.js"),
    output: {
        path: path.resolve("test", "dist"),
        filename: "[name].js",
        publicPath: 'http://game.qq.com/images/test', // 这里定义了publicPath
        chunkFilename: "[name].js"
    },
    // ......
}

这是配置文件,我们定义了publicPath

// manifest.js
    // ...

/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "http://game.qq.com/images/test"; // 赋值publicPath的值

    //...

// 

webpack把publicPath带进manifest.js

// 还是manifest.js
    // ...

/******/ 		script.src = __webpack_require__.p + "" + ({"0":"modA","1":"modC"}[chunkId]||chunkId) + ".js";

    // ...

还记得这行代码吗,这是动态加载异步chunk时拼 src 的部分。这里就把 __webpack_require__.p 拼在异步chunk的url上了

webpack_require.e

上面已经详细分析了~

webpack_require.d 和webpack_require.n

webpack从 2.0 开始原生支持 es6 modules ,也就是import,export语法,不需要借助babel编译。这会出现一个问题,es6 modules语法的import引入了 default 的概念,在 Commonjs模块 里是没有的,那么如果在一个Commonjs模块里引用es6 modules就会出问题,反之亦然。webpack对这种情况做了兼容处理,就是用 __webpack_require__.d__webpack_require__.n 来实现的,限于篇幅,就不在这里细讲了,大家可以阅读 webpack模块化原理-ES module 这篇文章,写的比较详细

webpack_require.nc

script属性 nonce 的值,如果你有使用的话,会在每个异步加载的script加上这个属性

A cryptographic nonce (number used once) to whitelist inline scripts in a script-src Content-Security-Policy . The server must generate a unique nonce value each time it transmits a policy. It is critical to provide a nonce that cannot be guessed as bypassing a resource's policy is otherwise trivial.

一些alias

webpack在 __webpack_require__ 上加了一些manifest.js里的变量引用,应该是给webpack内部js或者plugin加进来的js使用的:

  1. webpack_require .m modules的引用
  2. webpack_require .c installedModules的引用

如果你尝试在你的代码里使用这些变量或者require本身(不是用require来引入模块),webpack会把它编译成一个报错函数

一些 工具 函数的简写

  1. webpack_require .o Object.prototype.hasOwnProperty.call 的简写
  2. webpack_require .oe 异步加载chunk报错的函数

chunk与module的区别

可能很多同学搞不清楚chunk和module的区别,在这里特别说明一下

module的概念很简单,未编译的代码里每个js文件都是一个module,比如:

// entry.js
import a from './a.js';

console.log(a); // 1

// a.js
module.exports = 1;

这里entry.js和a.js都是module

那什么是 chunk 呢。先说简单的,如果你的代码既没有code split,也没有需要异步加载的module,这时编译出的js文件只有两个:

  1. manifest.js,也就是bootstrap代码
  2. 你的源代码编译后的js文件

它们都是chunk。有图为证:

webpack bootstrap源码解读

main chunk就是你的源码编译生成的,因为它是以入口文件为起点生成的,所以也叫 entry chunk

还记得在初始化部分 installedChunks 的初始化值么

/******/ 	// objects to store loaded and loading chunks
/******/ 	var installedChunks = {
/******/ 		1: 0
/******/ 	};

这里已经把id为 1 的chunk的值置成0了,说明这个chunk已经加载好了。what?这不是才开始初始化吗! 再看看上面的那张图,manifest这个chunk的id为1,manifest当然执行了~

再说复杂的,也就是有code split的情况,这时就不止有entry chunk了,还有因为code split产生的chunk。 code split的情形有两种:

  1. 通过CommonChunkPlugin分离出的chunk
  2. 异步模块产生的chunk

第2点的异步模块,指的是通过 require.ensure 或者 import() 引入的模块,这些模块因为是异步加载的,会被单独打包到一个文件,在 触发加载条件时才会加载这个chunk.js

ok,我们总结一下产生chunk的3种情形

  1. entry chunk 也就是入口文件产生的chunk,这个必有
  2. initial chunk 也就是manifest生成的chunk,这个也是必有
  3. normal chunk 也就是code split产生的chunk,这个得看你是否有用到code split,且他们是异步加载的

完整的manifest.js

/******/ (function(modules) { // webpackBootstrap
/******/ 	// install a JSONP callback for chunk loading
/******/ 	var parentJsonpFunction = window["webpackJsonp"];
/******/ 	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
/******/ 		// add "moreModules" to the modules object,
/******/ 		// then flag all "chunkIds" as loaded and fire callback
/******/ 		var moduleId, chunkId, i = 0, resolves = [], result;
/******/ 		for(;i < chunkIds.length; i++) {
/******/ 			chunkId = chunkIds[i];
/******/ 			if(installedChunks[chunkId]) {
/******/ 				resolves.push(installedChunks[chunkId][0]);
/******/ 			}
/******/ 			installedChunks[chunkId] = 0;
/******/ 		}
/******/ 		for(moduleId in moreModules) {
/******/ 			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
/******/ 				modules[moduleId] = moreModules[moduleId];
/******/ 			}
/******/ 		}
/******/ 		if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
/******/ 		while(resolves.length) {
/******/ 			resolves.shift()();
/******/ 		}
/******/ 		if(executeModules) {
/******/ 			for(i=0; i < executeModules.length; i++) {
/******/ 				result = __webpack_require__(__webpack_require__.s = executeModules[i]);
/******/ 			}
/******/ 		}
/******/ 		return result;
/******/ 	};
/******/
/******/ 	// The module cache
/******/ 	var installedModules = {};
/******/
/******/ 	// objects to store loaded and loading chunks
/******/ 	var installedChunks = {
/******/ 		1: 0
/******/ 	};
/******/
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/
/******/ 		// Check if module is in cache
/******/ 		if(installedModules[moduleId]) {
/******/ 			return installedModules[moduleId].exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = installedModules[moduleId] = {
/******/ 			i: moduleId,
/******/ 			l: false,
/******/ 			exports: {}
/******/ 		};
/******/
/******/ 		// Execute the module function
/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ 		// Flag the module as loaded
/******/ 		module.l = true;
/******/
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/
/******/
/******/ 	// expose the modules object (__webpack_modules__)
/******/ 	__webpack_require__.m = modules;
/******/
/******/ 	// expose the module cache
/******/ 	__webpack_require__.c = installedModules;
/******/
/******/ 	// define getter function for harmony exports
/******/ 	__webpack_require__.d = function(exports, name, getter) {
/******/ 		if(!__webpack_require__.o(exports, name)) {
/******/ 			Object.defineProperty(exports, name, {
/******/ 				configurable: false,
/******/ 				enumerable: true,
/******/ 				get: getter
/******/ 			});
/******/ 		}
/******/ 	};
/******/
/******/ 	// getDefaultExport function for compatibility with non-harmony modules
/******/ 	__webpack_require__.n = function(module) {
/******/ 		var getter = module && module.__esModule ?
/******/ 			function getDefault() { return module['default']; } :
/******/ 			function getModuleExports() { return module; };
/******/ 		__webpack_require__.d(getter, 'a', getter);
/******/ 		return getter;
/******/ 	};
/******/
/******/ 	// Object.prototype.hasOwnProperty.call
/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ 	// __webpack_public_path__
/******/ 	__webpack_require__.p = "";
/******/
/******/ 	// on error function for async loading
/******/ 	__webpack_require__.oe = function(err) { console.error(err); throw err; };
/******/ })
/************************************************************************/
/******/ ([]);

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Charlotte's Web

Charlotte's Web

E. B. White / Puffin Classics / 2010-6-3 / GBP 6.99

This is the story of a little girl named Fern who loved a little pig named Wilbur and of Wilbur's dear friend, Charlotte A. Cavatica, a beautiful large grey spider. With the unlikely help of Templeton......一起来看看 《Charlotte's Web》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具