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; };
/******/ })
/************************************************************************/
/******/ ([]);

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

查看所有标签

猜你喜欢:

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

不止情感设计

不止情感设计

陈华 / 电子工业出版社 / 2015-5-21 / 59.00

本书着眼于“设计&心理”两个主要的维度,围绕“创新式思维2.0”(共情—移情—定义—构思—建模—测试)的模式,分析如何“理解一款好的产品设计”、“如何了解用户需求”、“如何从需求来定义产品”的几个步骤,由浅入深地介绍设计师通过洞察和理解用户内在需求来指导产品创新和设计的理念。一起来看看 《不止情感设计》 这本书的介绍吧!

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具