自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Webpack 實現(xiàn) Tree shaking 的前世今生

開發(fā) 前端
Tree-shaking 實現(xiàn)機制快速瀏覽完官方文檔和一眾文章后,發(fā)現(xiàn) webpack 實現(xiàn) tree-shaking 的方式還不止一種!但是,都與 rollup 不同。

[[407597]]

前言

如果看過 rollup 系列的這篇文章 - 無用代碼去哪了?項目減重之 rollup 的 Tree-shaking,那你一定對 tree-shaking 不陌生了。如果對 tree-shaking 相關(guān)知識不熟悉,請先點開上面這篇文章花 5 分鐘了解一下:什么是 tree-shaking。

眾所周知,原本不支持 tree-shaking 的 Webpack 在它的 2.x 版本也實現(xiàn)了 tree-shaking,好奇心又來了,rollup 從一開始就自實現(xiàn)了 tree-shaking,而 Webpack 則是看到 rollup 的打包瘦身效果之后,到了 2.x 才實現(xiàn),那么二者實現(xiàn) tree-shaking 的原理是一樣的嗎?

因為這樣的疑問,就有了眼前這篇文章。

Tree-shaking 實現(xiàn)機制

快速瀏覽完官方文檔和一眾文章后,發(fā)現(xiàn) webpack 實現(xiàn) tree-shaking 的方式還不止一種!但是,都與 rollup 不同。

早期 webpack 的配置使用并不簡單,也因此曾有 webpack 配置工程師的戲稱,雖然現(xiàn)在 webpack 的配置被極大簡化了,webpack4 也宣稱 0 配置,但如果涉及復(fù)雜全面的打包功能,并非是 0 配置可以實現(xiàn)的。了解其功能原理及配置還是極為有用的,接下來就來了解一下 webpack 實現(xiàn) tree-shaking 的原理吧。

Tree-shaking -- rollup VS Webpack

  • rollup 是在編譯打包過程中分析程序流,得益于于 ES6 靜態(tài)模塊(exports 和 imports 不能在運行時修改),我們在打包時就可以確定哪些代碼時我們需要的。
  • webpack 本身在打包時只能標記未使用的代碼而不移除,而識別代碼未使用標記并完成 tree-shaking 的 其實是 UglifyJS、babili、terser 這類壓縮代碼的工具。簡單來說,就是壓縮工具讀取 webpack 打包結(jié)果,在壓縮之前移除 bundle 中未使用的代碼。

我們提到了標記未使用代碼,也提到了 UglifyJS、babili、terser 等壓縮工具,那么 webpack 與壓縮工具是怎么實現(xiàn) tree-shaking 的呢?先來了解下 webpack 中實現(xiàn) tree-shaking 的前世今生吧!

Webpack 實現(xiàn) tree-shaking 的 3 個階段

第一階段:UglifyJS

webpack 標記代碼 + babel 轉(zhuǎn)譯 ES5 --> UglifyJS 壓縮刪除無用代碼 關(guān)于最早版本的 Webpack 實現(xiàn) tree-shaking 可以參考這篇文章 如何在 Webpack 2 中使用 tree-shaking(鏈接地址見文末參考),掘金也有翻譯版,當然如果不愿意花時間考古,也可以看下面這一段總結(jié):

  • UglifyJS 不支持 ES6 及以上,需要用 Babel 將代碼編譯為 ES5,然后再用 UglifyJS 來清除無用代碼;
  • 通過 Babel 將代碼編譯為 ES5,但又要讓 ES6 模塊不受 Babel 預(yù)設(shè)(preset)的影響:配置 Babel 預(yù)設(shè)不轉(zhuǎn)換 module,對應(yīng)地配置 Webpack 的 plugins 配置;
  • 為避免副作用,將其標記為 pure(無副作用),以便 UglifyJS 能夠處理,主要是 webpack 的編譯過程阻止了對類進行 tree-shaking,它僅對函數(shù)起作用,后來通過支持將類編譯后的賦值標記為 @__PURE__解決了這個問題。
  1. // .babelrc 
  2.   "presets": [ 
  3.     ["env", { 
  4.       "loose"true, // 寬松模式 
  5.       "modules"false // 不轉(zhuǎn)換 module,保持 ES6 語法 
  6.     }] 
  7.   ] 
  1. // webpack.config.js 
  2. module: { 
  3.   rules: [ 
  4.     { test: /\.js$/, loader: 'babel-loader' } 
  5.   ] 
  6. }, 
  7.  
  8. plugins: [ 
  9.   new webpack.LoaderOptionsPlugin({ 
  10.     minimize: true
  11.     debug: false 
  12.   }), 
  13.   new webpack.optimize.UglifyJsPlugin({ 
  14.     compress: { 
  15.       warnings: true 
  16.     }, 
  17.     output: { 
  18.       comments: false 
  19.     }, 
  20.     sourceMap: false 
  21.   }) 

第二階段:BabelMinify

webpack 標記代碼 --> Babili(即 BabelMinify)壓縮刪除無用代碼 Babili 后來被重命名為 BabelMinify,是基于 Babel 的代碼壓縮工具,而 Babel 已經(jīng)通過我們的解析器 Babylon 理解了新語法,同時又在 babili 中集成了 UglifyJS 的壓縮功能,本質(zhì)上實現(xiàn)了和 UglifyJS 一樣的功能,但使用 babili 插件又不必再轉(zhuǎn)譯,而是直接壓縮,使代碼體積更小。

一般使用 Babili 替代 uglify 有 Babili 插件式和 babel-loader 預(yù)設(shè)兩種方式。在官方文檔最后有說明,Babel Minify 最適合針對最新的瀏覽器(具有完整的 ES6+ 支持),也可以與通常的 Babel es2015 預(yù)設(shè)一起使用,以首先向下編譯代碼。

在 webpack 中使用 babel-loader,然后再引入 minify 作為一個 preset 會比直接使用 BabelMinifyWebpackPlugin 插件(下一個就講到)執(zhí)行得更快。因為 babel-minify 處理的文件體積會更小。

第三階段:Terser

webpack 標記代碼 --> Terser 壓縮刪除無用代碼 (webpack5 已內(nèi)置) terser 是一個用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。如果你看過這個 issue(https://github.com/webpack-contrib/terser-webpack-plugin/issues/15),就會知道放棄 uglify 而投向 terser 懷抱的人越來越多,其原因也很清楚:

  • uglify 不再進行維護且不支持 ES6+ 語法
  • webpack 默認內(nèi)置配置了 terser 插件實現(xiàn)代碼壓縮 關(guān)于副作用,從 webpack 4 正式版本擴展了未使用模塊檢測能力,通過 package.json 的 "sideEffects" 屬性作為標記,向 compiler 提供提示,表明項目中的哪些文件是 "pure(純正 ES2015 模塊)",由此可以安全地刪除文件中未使用的部分。

webpack4 的時候還要手動配置一下壓縮插件,但最新的 webpack5 已經(jīng)內(nèi)置實現(xiàn) tree-shaking 啦!在生產(chǎn)環(huán)境下無需配置即可實現(xiàn) tree-shaking !

Webpack 的 Tree-shaking 流程

Webpack 標記代碼

總的來說,webpack 對代碼進行標記,主要是對 import & export 語句標記為 3 類:

  • 所有 import 標記為 /* harmony import */
  • 所有被使用過的 export 標記為/* harmony export ([type]) */,其中 [type] 和 webpack 內(nèi)部有關(guān),可能是 binding, immutable 等等
  • 沒被使用過的 export 標記為/* unused harmony export [FuncName] */,其中 [FuncName] 為 export 的方法名稱

首先我們要知道,為了正常運行業(yè)務(wù)項目,Webpack 需要將開發(fā)者編寫的業(yè)務(wù)代碼以及支撐、調(diào)配這些業(yè)務(wù)代碼的運行時一并打包到產(chǎn)物(bundle)中。落到 Webpack 源碼實現(xiàn)上,運行時的生成邏輯可以劃分為打包階段中的兩個步驟:

  • 依賴收集:遍歷代碼模塊并收集模塊的特性依賴,從而確定整個項目對 Webpack runtime 的依賴列表;
  • 生成:合并 runtime 的依賴列表,打包到最終輸出的 bundle。

顯然,對代碼的語句標記就發(fā)生在依賴收集的過程中。

在運行時環(huán)境標記所有 import:

  1. const exportsType = module.getExportsType( 
  2.  chunkGraph.moduleGraph, 
  3.  originModule.buildMeta.strictHarmonyModule 
  4. ); 
  5. runtimeRequirements.add(RuntimeGlobals.require); 
  6. const importContent = `/* harmony import */ ${optDeclaration}${importVar} = __webpack_require__(${moduleId});\n`; 
  7.  
  8. // 動態(tài)導(dǎo)入語法分析 
  9. if (exportsType === "dynamic") { 
  10.  runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport); 
  11.  return [ 
  12.   importContent, // 標記/* harmony import */ 
  13.   `/* harmony import */ ${optDeclaration}${importVar}_default = /*#__PURE__*/${RuntimeGlobals.compatGetDefaultExport}(${importVar});\n` // 通過 /*#__PURE__*/ 注釋可以告訴 webpack 一個函數(shù)調(diào)用是無副作用的 
  14.  ]; // 返回 import 語句和 compat 語句 

在運行時環(huán)境標記所有被使用過的和未被使用的 export:

  1. // 在運行時狀態(tài)定義 property getters 
  2.  generate() { 
  3.  const { runtimeTemplate } = this.compilation; 
  4.  const fn = RuntimeGlobals.definePropertyGetters; 
  5.  return Template.asString([ 
  6.   "// define getter functions for harmony exports"
  7.   `${fn} = ${runtimeTemplate.basicFunction("exports, definition", [ 
  8.    `for(var key in definition) {`, 
  9.    Template.indent([ 
  10.     `if(${RuntimeGlobals.hasOwnProperty}(definition, key) && !${RuntimeGlobals.hasOwnProperty}(exports, key)) {`, 
  11.     Template.indent([ 
  12.      "Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });" 
  13.     ]), 
  14.     "}" 
  15.    ]), 
  16.    "}" 
  17.   ])};` 
  18.  ]); 
  19.   
  20.  // 輸入為 generate 上下文 
  21.  getContent({ runtimeTemplate, runtimeRequirements }) { 
  22.  runtimeRequirements.add(RuntimeGlobals.exports); 
  23.  runtimeRequirements.add(RuntimeGlobals.definePropertyGetters); 
  24.  
  25.  const unusedPart = 
  26.   this.unusedExports.size > 1 
  27.    ? `/* unused harmony exports ${joinIterableWithComma( 
  28.      this.unusedExports 
  29.      )} */\n` 
  30.    : this.unusedExports.size > 0 
  31.    ? `/* unused harmony export ${first(this.unusedExports)} */\n` 
  32.    : ""
  33.  const definitions = []; 
  34.  for (const [key, value] of this.exportMap) { 
  35.   definitions.push( 
  36.    `\n/* harmony export */   ${JSON.stringify( 
  37.     key 
  38.    )}: ${runtimeTemplate.returningFunction(value)}` 
  39.   ); 
  40.  } 
  41.  const definePart = 
  42.   this.exportMap.size > 0 
  43.    ? `/* harmony export */ ${RuntimeGlobals.definePropertyGetters}(${ 
  44.      this.exportsArgument 
  45.      }, {${definitions.join(",")}\n/* harmony export */ });\n` 
  46.    : ""
  47.  return `${definePart}${unusedPart}`; // 作為初始化代碼包含的源代碼 

壓縮清除大法

UglifyJS

以 UglifyJS 為例,UglifyJS 是一個 js 解釋器、最小化器、壓縮器、美化器工具集(parser, minifier, compressor or beautifier toolkit)。具體介紹可以查看下 UglifyJS 中文手冊。

如果不想瀏覽這么一大長篇文檔,可以看干凈利落、直指 tree-shaking 的壓縮配置參數(shù)總結(jié)吧!

  1. dead_code -- 移除沒被引用的代碼 // 是不是很眼熟!無用代碼!
  2. drop_debugger -- 移除 debugger
  3. unused -- 干掉沒有被引用的函數(shù)和變量。(除非設(shè)置"keep_assign",否則變量的簡單直接賦值也不算被引用。)
  4. toplevel -- 干掉頂層作用域中沒有被引用的函數(shù) ("funcs")和/或變量("vars") (默認是 false , true 的話即函數(shù)變量都干掉)
  5. warnings -- 當刪除沒有用處的代碼時,顯示警告 // 還挺貼心有么有~
  6. pure_getters -- 默認是 false. 如果你傳入 true,UglifyJS 會假設(shè)對象屬性的引用(例如 foo.bar 或 foo["bar"])沒有函數(shù)副作用。
  7. pure_funcs -- 默認 null. 你可以傳入一個名字的數(shù)組,UglifyJS 會假設(shè)這些函數(shù)沒有函數(shù)副作用。

舉個栗子:

  1. plugins: [ 
  2.   new UglifyJSPlugin({ 
  3.     uglifyOptions: { 
  4.       compress: { 
  5.           // 這樣該函數(shù)會被認為沒有函數(shù)副作用,整個聲明會被廢棄。在目前的執(zhí)行情況下,會增加開銷(壓縮會變慢)。 
  6.           pure_funcs: ['Math.floor'
  7.       } 
  8.     } 
  9.   }) 
  10. ], 

Tip:假如名字在作用域中重新定義,不會再次檢測。例如 var q = Math.floor(a/b),假如變量 q 沒有被引用,UglifyJS 會干掉它,但 Math.floor(a/b)會被保留,沒有人知道它是干嘛的。

  • side_effects -- 默認 true. 傳 false 禁用丟棄純函數(shù)。如果一個函數(shù)被調(diào)用前有一段/@PURE/ or /#PURE/ 注釋,該函數(shù)會被標注為純函數(shù)。例如 /@PURE/foo();

事實上,在這么多的壓縮配置中,除了要解決副作用問題要手動配置以外,僅使用 UglifyJS 默認配置即可去除無用標記代碼以實現(xiàn) tree-shaking。

terser

以 terser 為例,terser 是一個用于 ES6+ 的 JavaScript 解析器和 mangler/compressor 工具包。具體可查看官方文檔。雖然沒有中文文檔,但是一眼掃過去也可以看出來配置參數(shù)和 UglifyJS 沒有太大區(qū)別。當然很明顯地多了一些參數(shù):

  • arrows -- 如果轉(zhuǎn)換后的代碼更短,類和對象字面量方法也將被轉(zhuǎn)換為箭頭表達式
  • ecma -- 通過 ES2015 或 更高版本來啟用壓縮選項,將 ES5 代碼轉(zhuǎn)換為更小的 ES6+等效形式 顯然是因為 terser 支持 ES6+ 語法,這也是它淘汰 UglifyJS 的優(yōu)勢之一。

壓縮性能 PK

目前 Webpack 已經(jīng)更新到了版本 5.X,已經(jīng)將 terser 插件默認內(nèi)置且無需配置,雖然生產(chǎn)環(huán)境下默認使用 TerserPlugin ,并且也是代碼壓縮方面比較好的選擇,但是還有一些其他可選擇項。等等,我們的主題不是 tree-shaking 嗎?怎么在壓縮工具的路上突然越走越遠...

本質(zhì)上,實現(xiàn) tree-shaking 的還是壓縮工具,所以我們來看壓縮工具的性能好像也沒毛病!

TIP:壓縮是在生產(chǎn)環(huán)境中生效的,所以生產(chǎn)環(huán)境下才能 tree-shaking。下面 3 個可配置插件要求 webpack 版本至少在 V4+。

UglifyjsWebpackPlugin

基本的使用方式也更加簡單:

  1. // webpack.config.js 
  2. const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 
  3.  
  4. module.exports = { 
  5.   optimization: { 
  6.     minimizer: [new UglifyJsPlugin()], 
  7.   }, 
  8. }; 
  9.  
  10. const UglifyJsPlugin = require('uglifyjs-webpack-plugin'
  11.  
  12. module.exports = { 
  13.   plugins: [ 
  14.     new UglifyJsPlugin() 
  15.   ] 

 

BabelMinifyWebpackPlugin

一般使用 babili 替代 UglifyJS 有 Babili 插件式和 babel-loader 預(yù)設(shè)兩種方式。

Babili 插件式

只要用 Babili 插件替代 uglify 即可,此時也不需要 babel-loader 了:

  1. // webpack.config.js 
  2. const MinifyPlugin = require("babel-minify-webpack-plugin"); 
  3. module.exports = { 
  4.   plugins: [ 
  5.     new MinifyPlugin(minifyOpts, pluginOpts) 
  6.   ] 

babel-loader 預(yù)設(shè)

在官方文檔最后有說明,Babel Minify 最適合針對最新的瀏覽器(具有完整的 ES6+ 支持),也可以與通常的 Babel es2015 預(yù)設(shè)一起使用,以首先向下編譯代碼。

在 webpack 中使用 babel-loader,然后再引入 minify 作為一個 preset 會比直接使用 BabelMinifyWebpackPlugin 插件執(zhí)行得更快。因為 babel-minify 處理的文件體積會更小。

即在.babelrc 中配置如下:

  1.   "presets": ["es2015"], 
  2.   "env": { 
  3.     "production": { 
  4.       "presets": ["minify"
  5.     } 
  6.   } 

但 BabelMinifyWebpackPlugin 插件存在必定有其無法替代的作用:

  • webpack loader 對單個文件進行操作, minify preset 作為一個 webpack loader 會把每個文件視為在瀏覽器全局范圍內(nèi)直接執(zhí)行(默認情況下),并不會優(yōu)化頂級作用域內(nèi)的某些內(nèi)容;
  • 當排除 node_modules 不通過 babel-loader 運行時,babel-minify 優(yōu)化不會應(yīng)用于被排除的文件;
  • 當使用 babel-loader 時,由 webpack 為模塊系統(tǒng)生成的代碼不會通過 babel-minify 進行優(yōu)化;
  • webpack 插件可以在整個 chunk/bundle 輸出上運行,并且可以優(yōu)化整個 bundle。

采用第一種方式:

TerserWebpackPlugin

同 uglify 和 babelMinify 插件一樣,terser 插件配置使用也十分簡單。

  1. webpack.config.js 
  2. const TerserPlugin = require("terser-webpack-plugin"); 
  3.  
  4. module.exports = { 
  5.   optimization: { 
  6.     minimize: true
  7.     minimizer: [new TerserPlugin()], 
  8.   }, 
  9. }; 

 

企業(yè)微信截圖_16247735356260.png

看上去結(jié)果是符合預(yù)期的,又因為我的文件代碼本身體積就小,所以壓縮包體積上的優(yōu)勢其實并不明顯,但壓縮時間上還是比較明顯的。

官方數(shù)據(jù)性能對比

再來康康 bableMinify 文檔 中給出的對比吧:

打包 react:

圖片

打包 vue:

打包 lodash:

圖片

打包 three.js:

圖片

小結(jié)

先讓我們來看看 issue 區(qū)網(wǎng)友們是怎么說的:

圖片

大意是 terser 壓縮性能相較于 uglify 提升了三倍!Nice!

大意是說:鑒于 terser-webpack-plugin 得到維護并且有更多的正確性修復(fù),絕對是首選 -- 即使沒有性能改進(事實上還是有所改進的),也值得切換。最后一句話總結(jié):webpack 打包 + terser 壓縮才是最終的不二之選!webpack5 內(nèi)置 terser 說明了一切!

處理 Side Effects

「副作用」的定義是,在導(dǎo)入時會執(zhí)行特殊行為的代碼,而不是僅僅暴露一個 export 或多個 export。舉例說明,例如 polyfill,它影響全局作用域,并且通常不提供 export。

關(guān)于副作用在 rollup 中也已經(jīng)介紹過。有些模塊導(dǎo)入,只要被引入,就會對應(yīng)用程序產(chǎn)生重要的影響。比如全局樣式表,或者設(shè)置全局配置的 JavaScript 文件就是很好的例子。

Webpack 認為這樣的文件有“副作用”,具有副作用的文件不應(yīng)該做 tree-shaking,因為這將破壞整個應(yīng)用程序。webpack 的 tree-shaking 在副作用處理方面稍顯遜色,它可以簡單的判斷變量后續(xù)是否被引用、修改,但是不能判斷一個變量完整的修改過程,不知道它是否已經(jīng)指向了外部變量,所以很多有可能會產(chǎn)生副作用的代碼,都只能保守的不刪除。

幸運的是,我們可以通過配置項目,告訴 Webpack 哪些代碼是沒有副作用的,可以進行 tree-shaking。

配置參數(shù)

在項目的 package.json 文件中,添加 "sideEffects" 屬性。package.json 有一個特殊的屬性 sideEffects,就是為處理副作用而存在的 -- 向 webpack 的 compiler 提供提示哪些代碼是“純粹部分”。它有三個可能的值:

  • true 是默認值,如果不指定其他值的話。這意味著所有的文件都有副作用,也就是沒有一個文件可以 tree-shaking。
  • false 告訴 Webpack 沒有文件有副作用,所有文件都可以 tree-shaking。
  • 第三個值 […] 是文件路徑數(shù)組。它告訴 webpack,除了數(shù)組中包含的文件外,你的任何文件都沒有副作用。因此,除了指定的文件之外,其他文件都可以安全地進行 tree-shaking。
  1.   "name""your-project"
  2.   "sideEffects"false 
  3.   // "sideEffects": [ // 數(shù)組方式支持相關(guān)文件的相對路徑、絕對路徑和 glob 模式 
  4.   //  "./src/some-side-effectful-file.js"
  5.   //  "*.css" 
  6.   //] 

每個項目都必須將 sideEffects 屬性設(shè)置為 false 或文件路徑數(shù)組,如果你的代碼確實有一些副作用,那么可以改為提供一個數(shù)組,在工作中需要正確配置 sideEffects 標記。

代碼中標記

可以通過 /#PURE/ 注釋可以告訴 webpack 一個函數(shù)調(diào)用是無副作用的。在函數(shù)調(diào)用之前,用來標記它們是無副作用的(pure)。傳到函數(shù)中的入?yún)⑹菬o法被剛才的注釋所標記,需要單獨每一個標記才可以。如果一個沒被使用的變量定義的初始值被認為是無副作用的(pure),它會被標記為死代碼,不會被執(zhí)行且會被壓縮工具清除掉。當 optimization.innerGraph 被設(shè)置成 true 這個行為被會開啟,而在 webpack5.x 中optimization.innerGraph 默認為 true。

語法使用層面

  • 首先,mode 為 production 模式下才會啟用更多優(yōu)化項,包括我們本文講的壓縮代碼與 tree shaking;
  • 使用 ES2015 模塊語法(即 import 和 export);
  • 確保沒有編譯器將 ES2015 模塊語法轉(zhuǎn)換為 CommonJS 的,把 presets 中的 modules 設(shè)置為 false,告訴 babel 不要編譯模塊代碼。

總結(jié)

  • 如果是開發(fā) JavaScript 庫,使用 rollup!并且提供 ES6 module 的版本,入口文件地址設(shè)置到 package.json 的 module 字段;
  • 使用 webpack 哪怕是舊版本可以優(yōu)先考慮 terser 插件作為壓縮工具;
  • 為避免副作用,盡量不寫帶有副作用的代碼,使用 ES2015 模塊語法;
  • 在項目 package.json 文件中,添加一個 sideEffects 入口,設(shè)置 sideEffects 屬性為 false,也可以通過 /#PURE/ 注釋強制刪除一些認為不會產(chǎn)生副作用的代碼;
  • 在 Webpack 中還要額外引入一個能夠刪除未引用代碼(dead code)的壓縮工具(eg. Terser)。

參考資料

  • 如何在 Webpack 2 中使用 tree-shaking()https://blog.craftlab.hu/how-to-do-proper-tree-shaking-in-webpack-2-e27852af8b21
  • 你的 Tree-Shaking 并沒什么卵用(https://zhuanlan.zhihu.com/p/32831172)
  • UglifyJS 中文手冊(https://github.com/LiPinghai/UglifyJSDocCN/blob/master/README.md)
  • Webpack 4 Tree Shaking 終極優(yōu)化指南(https://juejin.cn/post/6844903998634328072#heading-5)
  • Webpack 中文文檔 Tree-shaking(https://www.webpackjs.com/guides/tree-shaking/)

 

責(zé)任編輯:姜華 來源: 微醫(yī)大前端技術(shù)
相關(guān)推薦

2022-02-10 14:23:16

WebpackJavaScript

2021-08-26 10:30:29

WebpackTree-Shakin前端

2011-08-23 09:52:31

CSS

2014-07-30 10:55:27

2025-02-12 11:25:39

2015-11-18 14:14:11

OPNFVNFV

2016-12-29 13:34:04

阿爾法狗圍棋計算機

2013-05-23 16:23:42

Windows Azu微軟公有云

2014-07-15 10:31:07

asyncawait

2021-06-17 07:08:19

Tapablewebpack JavaScript

2012-05-18 16:54:21

FedoraFedora 17

2014-07-21 12:57:25

諾基亞微軟裁員

2016-12-29 18:21:01

2019-06-04 09:00:07

Jenkins X開源開發(fā)人員

2016-11-08 19:19:06

2016-11-03 13:33:31

2013-11-14 16:03:23

Android設(shè)計Android Des

2011-05-13 09:43:27

產(chǎn)品經(jīng)理PM

2019-08-05 10:08:25

軟件操作系統(tǒng)程序員

2019-04-28 09:34:06

點贊
收藏

51CTO技術(shù)棧公眾號