為支持兩個語言版本,我基于谷歌翻譯API寫了一款自動翻譯的 webpack 插件

大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列

本文來自讀者@漫思維 投稿授權

原文鏈接:https://juejin.cn/post/7072677637117706270

1前言

以下我會列舉出我業務中遇到的問題難點及相對應的解決方法,解釋簡繁體插件怎么誕生的整個過程

2背景

目前開發工作有大量的營銷活動需要編寫,特點是小而多,同時現階段項目需要做大陸與港臺兩個版本

3現階段實現的方案

  1. 先做完大陸版本,最后再復刻一份代碼, 改成港臺版本

  2. 將項目中的漢字、價格、登錄方式進行替換。

4存在的問題

  1. 首先復制來復制去就不是一個很好的方案,容易復制出問題,其次兩個版本都是需要同一個時間點上線,復刻代碼的代碼的時機存在問題,如果復刻的過早,如果提測階段大陸版本有bug, 那么就需要修改兩份bug, 如果復刻的過晚那么會存在港臺版本測試時間不足,也易導致問題發生。

  2. 簡繁體轉換,都是將簡體手動復制到谷歌翻譯網頁端中翻譯好,再手動替換,繁瑣且工程量大, 登錄方式需要單獨的復制一份。

5兩個版本之間存在以下不同點

  1. 登錄方式的不同, 大陸主要是用賬號密碼登錄,而港臺使用谷歌、臉書、蘋果登錄

  2. 價格、單位不同,¥ 與 NT$

  3. 漢字的形式不同,中文簡體與中文繁體

核心問題在于復刻出一份項目存在的工作量與潛在風險較大,所以需要將兩個項目合成一個項目,怎么解決?

6解決方案

1. 將兩個項目合并成一個項目

如果需要將兩個項目合成一個項目,并解決以上分析出來的不同點,那么顯而易見,需要有個一標識去區分,那么使用環境變量解決這個問題是非常合適的,以vue項目舉例, 可以編寫對應的環境變量配置。

大陸版本生產環境:.env

VUE_APP_ENV=prod
VUE_APP_PUBLIC_PATH=/mainland

大陸版本開發環境:.env

VUE_APP_ENV=dev
VUE_APP_PUBLIC_PATH=/mainland

港臺版本開發環境:.env.ht

VUE_APP_ENV=ht
VUE_APP_PUBLIC_PATH=/ht
NODE_ENV=production

package.json

"serve":?"vue-cli-service?serve",
"build":?"vue-cli-service?build",
"build:ht":?"vue-cli-service?build?--mode?ht",

可以看到這里使用了一個自定義變量 VUE_APP_ENV, 在項目代碼中就可以使用 process.env.VUE_APP_ENV 去做區分當前是大陸還是港臺了,同時為什么不使用NODE_ENV作為變量,因為該變量往往會有其他用途,如當NODE_ENV設置為production 時,打包時會做一些如壓縮等優化操作。

注: 港臺版本不做測試環境的區分,因為往往大陸版的邏輯沒有問題,港臺版的就沒有問題,所以只需要基于大陸版開發,港臺版只需要最后打包一次即可 **(測試環境可選,只需要多添加一個配置即可)**。

其他注意點: process.env.VUE_APP_ENV通常只能在node環境下才能訪問的,但是vue-cli創建項目會自動將.env里的變量注入到運行時環境中,也就是使用一個全局變量存起來,通常是使用webpack的define-plugin插件實現的。

解決了環境變量的問題,接下來的工作就比較好進行了。

2. 解決登錄方式的不同

將兩套登錄封裝成兩個不同的組件,因為登錄往往涉及到一些全局狀態,項目一般都會使用vuex等全局狀態管理工具,所以默認使用vuex儲存狀態,把整個包含登錄邏輯的代碼制作成一個項目的基礎模板,使用自定義腳手架拉取即可,同時注意使用vuex時,為登錄相關的狀態,放置到一個module下,這樣基于該模板創建項目后, 每個項目的其它狀態單獨再寫module即可,避免修改登錄的module

自定義腳手架:交互式創建項目,輸入一些選項,如項目名稱,項目描述之類的,再從gitlab等遠程倉庫拉取已經寫好的模板,將模板中的一些特定變量,使用模板引擎將模板中的項目名稱等替換,最終產生一個新的項目。(腳手架還有其他用途,這里只描述使用它創建一個簡單的項目)

  • 沒有腳手架那就只能使用git clone 下來后再修改項目名稱之類的東西,會增加一點額外的工作,但不影響不大。

封裝的部分邏輯:

比如大陸的登錄組件叫做 mainlandLogin, 港臺的登錄組件叫 htLogin,再寫一個 login組件將他們整合,通過環境變量進行區分引入不同的組件,使用component動態加載對應的登錄組件如下:

login.vue:

<component?:is="currentLogin"?@sure="sure"?cancel="cancel"></component>data:{return?{currentLogin:?process.env.VUE_APP_ENV?===?'ht'???'mainlandLogin'?:?'htLogin'}
},
components:?{mainlandLogin:?()?=>?import("./components/mainlandLogin.vue"),htLogin:?()?=>?import("./components/htLogin.vue"),
},
method:{sure(){this.$emit('sure')},cancel(){this.$emit('cancel')}
}

注意: 引入組件的方式使用動態加載,打包時會將兩個組件打包成兩個單獨的chunk, 因為大陸版本與港臺版本只會用到一種登錄,另一個用不到的不需要引入

經過如上操作將登錄的組件封裝好以后使用起來就很簡單了

<login?@sure="sure"?cancel="cancel"></login>

3. 解決價格不一致問題

與登錄一樣,根據環境變量區分即可,在原來大陸版本的商品JSON中加入一個字段即可如htPrice

const?commodityList?=?[{id:?1name:?"xxx",count:1,price:1,htPrice:?2}
]

遍歷的時候還是根據process.env.VUE_APP_ENV === 'ht'進行顯示對應價格與單位

{{?isHt???`${commodity.htPrice}?NT$`?:?`${commodity.price}?¥`?}}data()?{return?{isHt:?process.env.VUE_APP_ENV?===?'ht'}
}

4. 簡繁體轉換

解決了兩個項目合并成一個項目和登錄、價格、單位不一致的問題,最后只剩下簡體轉繁體,也是最難解決的一部分,經過了多次技術調研沒有找到合適的方案,最后只能自己寫一套。

1. 使用i18n, 維護兩套語言文件

優點: 國際化使用的最多的一個庫,不用改動代碼中的文字,使用變量替換,只需維護兩套語言文件,改動點集中在一個文件中

缺點: 使用變量進行替換一定程度上增加了代碼的復雜性,無法省去手動復制簡體去翻譯在額外寫入特定的語言文件這一過程,對于這個場景不是一個最好的方案

2. 采用:language-tw-loader

優點: ? 看似 可以自動化將簡體轉換成繁體,方便快捷

缺點: 在使用時發現一個致命的缺點, 無法準確替換,原因: 不同的詞組,同一個詞可能對應多個字形,如:聯系 -> 聯繫, 系鞋帶 -> 系鞋帶。

基本原理: 列舉常用的中文簡體與繁體,一一對應,逐一替換, 如下圖所示:

80a79cfa5f2de7008fdd1db48d8090a1.png
image.png

3. 采用 v-google-translate優點: 運行時采用谷歌翻譯,自動將網頁的簡體翻譯成繁體

缺點: 因為是運行時轉義,所以頁面始終會先展示簡體,過一段時間再顯示繁體

綜上所述: 現有的一些方案存在以下幾個問題

  1. 需要維護額外的語言文件,使用變量替換文字

  2. 編譯時轉換無法正確轉換,運行時轉換有延時

為了解決以上問題:

1. 無需寫多套語言文件,正常開發使用中文進行編寫即可

需要一個翻譯的API,且翻譯要準確,經測試簡繁體轉換谷歌翻譯是最準確的。

2. 在編譯時轉換

編寫打包工具的plugin,這里主要以webpack為打包工具,所以需要編寫一個webpackplugin

翻譯API

需要一個免費、準確、且不易掛的翻譯服務,但是谷歌翻譯API是需要付費的,有錢付費的很方便就能享受這個服務,但是為了一個簡體轉繁體產生額外的支出,不太現實。

開源項目中有很多的免費谷歌API, 但都是去嘗試模擬生成其加密token,進行請求,服務很容易掛掉,所以很多 直接變成了沒有

但是!!!你要記得,谷歌翻譯是提供免費的網頁版的!

所以只需要打開一個瀏覽器,填入需要翻譯的文字,獲取翻譯后的文字即可,只不過需要程序自動幫我們打開一個瀏覽器,你沒想錯,已經有很成熟的方案puppeteer 就是干這件事情的。

所以最終采用: 基于puppeteer的訪問谷歌https://translate.google.cn 獲得翻譯結果,比其他方案都要穩定。

同時已有大佬寫了一個基于puppeteer的轉換服務 translateer,感興趣的可以看看其源碼,也不復雜。

但是注意,基于 translateer 啟動API服務, 存在幾個可以優化的點:

先看下為什么需要優化, 首先我們得要知道谷歌翻譯網頁端最大支持多少字符,測試得知如下最大支持一頁最大支持 5000字符,超過的部分可以翻頁。

f9ce54da2507677c4145b48b0f99892c.png再以上左側輸入框內輸入源文本,該網頁會發送一個post請求,一小會延遲右側出現翻譯后的內容,同時注意導航欄上的鏈接會變成如下形式:

https://translate.google.cn/?sl=zh-CN&tl=zh-TW&text=哈哈哈&op=translate

上面幾個參數分別的含義

sl:?源語言;?tl:?目標語言;?text:?翻譯的文本;?op:?translate?(翻譯)

如果直接使用以上鏈接進行請求,經過測試,將text值替換為'1'.repeat(16346)16346 個字符時 (該數值不包括url上其它字符,算上其它字符,那么總的url長度是16411) ,谷歌接口會返回400錯誤。

50160e246e49342171d61f14cade34d2.png
image.png

值得提的是: 看了很多的文章都說chromeget請求最大字符長度限制是20488182,但是都不太準確,上述測試就可以證明,總長度少于16411 谷歌翻譯依舊可以正常訪問,超過以后還是由谷歌翻譯對應的后臺服務器拋出的400 錯誤。

參考了GET請求的長度限制, 以下幾點是可以知道的:

1、首先即使有長度限制,也是限制的是整個URI長度,而不僅僅是你的參數值數據長度。

2、HTTP協議從未規定GET/POST的請求長度限制是多少

3、所謂的請求長度限制是由瀏覽器和web服務器決定和設置的,瀏覽器和web服務器的設定均不一樣

所以瀏覽器到底限制的是多少字符呢,暫時還沒有找到正確答案,有知道的大佬可以幫忙解釋一下

測試所用的谷歌瀏覽器版本: 98.0.4758.102(正式版本)(64 位)

分析了以上基本的限制,接下來看看translateer 的實現:

translateer 服務啟動時創建一個 PagePool頁面池,開啟5tab頁面并且都跳轉至https://translate.google.cn/, 以下為刪減后的部分代碼:

export?default?class?PagePool?{private?_pages:?Page[]?=?[];private?_pagesInUse:?Page[]?=?[];constructor(private?browser:?Browser,?private?pageCount:?number?=?5)?{pagePool?=?this;}public?async?init()?{this._pages?=?await?Promise.all([...Array(this.pageCount)].map(()?=>this.browser.newPage().then(async?(page)?=>?{await?page.goto("https://translate.google.cn/",?{waitUntil:?"networkidle2",});return?page;})));}
}

然后使用fastify啟動一個Node服務器,對外提供一個get請求API。以下為刪減后的部分代碼:

fastify.get("/",async?(request,?reply)?=>?{const?{?text,?from?=?"auto",?to?=?"zh-CN",?lite?=?false?}?=?request.query;const?page?=?pagePool.getPage();await?page.evaluate(([from,?to,?text])?=>?{location.href?=?`?sl=${from}&tl=${to}&text=${encodeURIComponent(text)}`;},[from,?to,?text]);//?translating...await?page.waitForSelector(`span[lang=${to}]`);//?get?translated?textlet?result?=?await?page.evaluate((to)?=>(document.querySelectorAll(`span[lang=${to}]`)[0]?as?HTMLElement).innerText,to);
}

傳入sl: 源語言; tl: 目標語言; text: 翻譯的文本 這幾個參數,location.href 跳轉至

?sl=${from}&tl=${to}&text=${encodeURIComponent(text)} 從而獲得右側輸入框的返回結果。

分析了其基本的實現原理,接下來分析其中存在的坑點。

location.href 是個get請求,經過上面的分析暫時不知道瀏覽器get請求的字符長度限制,但是已經知道谷歌后臺服務的對請求長度的限制為16411, 再粗略減去411個字符作為url的其他字符長度, 那么每次的翻譯文本最大支持長度就為16000個字符。

而如上代碼對text進行encodeURIComponent 編碼 (get請求默認也會對中文及其它特殊字符進行編碼)

需要注意的是中文一個字符編碼后為9個字符 => %E8%81%94, 那么16000 / 9 約等于 1777個漢字

階段總結:

由于谷歌翻譯網頁版的一些限制,直接使用get請求,一次最大支持翻譯1777個漢字, 而在輸入框內模擬輸入漢字無字符長度限制,一頁最大支持5000 字符,超出的部分可進行翻頁。

需要達到的效果是一次翻譯最少要能翻譯5000個字符,盡量少請求次數,能減少翻譯的時間,進而加快插件編譯的速度,所以需要開始改進 translateer

  1. 使用fastify創建一個新的post請求API

export?const?post?=?((fastify,?opts,?done)?=>?{fastify.post('/',async?(request,?reply)?=>?{...more...});done();
});
  1. 跳轉時只添加參數sl源語言tl目標語言不加text參數

await?page.evaluate(([from,?to])?=>?{location.href?=?`?sl=${from}&tl=${to}`;},[from,?to]);
  1. 選中谷歌翻譯頁面左側的文本輸入框,并將需要翻譯的文本賦值給輸入框,并且需要使用page.type鍵入一個空字符,觸發一次文本框的input事件,網頁才會執行翻譯。

await?page.waitForSelector(`span[lang=${from}]?textarea`);const?fromEle?=?await?page.$(`span[lang=${from}]?textarea`);await?page.evaluate((el,?text)?=>?{el.value=?text},fromEle,?text)//?模擬一次輸入觸發input事件,使得谷歌翻譯可以翻譯await?page.type(`span[lang=${from}]?textarea`,?'?');//?translating...await?page.waitForSelector(`span[lang=${to}]`);//?get?translated?textconst?result?=?await?page.evaluate((to)?=>(document.querySelectorAll(`span[lang=${to}]`)[0]?as?HTMLElement).innerText,to);

這里有個坑點,就是 page.type 是模擬用戶輸入所以他會一個字一個字的輸入,一開始的時候我使用它去給文本輸入框賦值,文本過長時,輸入的時間巨長,當時不知道怎么處理,為此我還專門提了個issue, 被指導后才改寫成現在的寫法: ?issues

總結:

前面提到,超過5000字符可以進行翻頁,這里沒有進行翻譯處理,目前限制就每次請求翻譯5000個字符已經夠用,超過5000再請求一次翻譯接口 (后續可處理一下翻頁,不管多長的字符都一次翻譯完畢, 不過還需要進一步對比兩者的所用時間長短)

最后以上修改過的代碼github地址: Translateer

translate-language-webpack-plugin

解決了翻譯API的問題,剩下的事情就只剩將代碼中的中文簡體轉換成繁體了,由于打包工具使用的webpack, 所以編寫webpack plugin 進行讀取中文并替換, 同時需要支持webpack5.0webpack4.0版本,以下以5.0版本為例:

首先理一下該插件的思路

  1. 編寫webpack插件

  2. 讀取代碼中所有的中文

  3. 請求翻譯API, 獲得翻譯后的結果

  4. 將翻譯后的結果寫入至代碼中

  5. 額外的功能:將每次讀取的源文本與目標文本輸出至日志中, 特別是在翻譯返回的文本長度與源文本長度不一致時用于對照。

接下來一步步實現上述功能

1. 第一步需要編寫一個插件,怎么寫?這是個問題

4.0版本5.0版本 的鉤子是不一樣的,而且很多,這里不會介紹webpack plugin中每個鉤子的含義,不是兩句話能說的清楚的, 網上有很多介紹的如揭秘webpack插件工作流程和原理,要想快速的寫一個插件,那么最快的方式就是參考現有的成熟的插件,我在編寫的時候就是直接參照的html-webpack-plugin, 4.0版本5.0版本都是參照其對應版本寫的。

tips: ?看開源項目的源碼的意義就在于此,可以學到很多的成熟的解決方案,可以稍微少踩一點坑, 所以最基本也需要學會如何找入口文件,如何調試代碼。

部分代碼如下,參考如下注釋:

const?{?sources,?Compilation?}?=?require('webpack');
//?日志輸出文件
const?TRANSFROMSOURCETARGET?=?'transform-source-target.txt';
//?谷歌翻譯一次最大支持字符
const?googleMaxCharLimit?=?5000;
//?插件名稱
const?pluginName?=?'TransformLanguageWebpackPlugin';class?TransformLanguageWebpackPlugin?{constructor(options?=?{})?{//?默認的一些參數const?defaultOptions?=?{?translateApiUrl:?'',?from:?'zh-CN',?to:?'zh-TW',?separator:?'-',?regex:?/[\u4e00-\u9fa5]/g,?outputTxt:?false,?limit:?googleMaxCharLimit,};//?translateApiUrl?翻譯API必須傳if?(!options.translateApiUrl)throw?new?ReferenceError('The?translateApiUrl?parameter?is?required');//?將傳入的參數與默認參數合并this.options?=?{?...defaultOptions,?...options?};}//?添加apply方法,供webpack調用apply(compiler)?{const?{separator,?translateApiUrl,?from,?to,?regex,?outputTxt,?limit}?=?this.options;//?監聽compiler?的thisCompilation?鉤子compiler.hooks.thisCompilation.tap(pluginName,?(compilation)?=>?{//?監聽compilation?的processAssets?鉤子compilation.hooks.processAssets.tapAsync({name:?pluginName,// stage 代表資源處理的階段, PROCESS_ASSETS_STAGE_ANALYSE:analyze the existing assets.stage:?Compilation.PROCESS_ASSETS_STAGE_ANALYSE,},//?assets?代表所有chunk文件`路徑及內容async?(assets,?callback)?=>?{// TODO:在此處填充要實現的功能})})}
}

以上為該插件的基本結構, webpack5.0processAssets鉤子用于處理文件,我們主要看一下 Compilation.PROCESS_ASSETS_STAGE_ANALYSE階段assets 中有什么. 以提供的github倉庫中提供的例子為例

9c67f6c82528eaa6d40a2b5174d71e80.png可以看到assets就是最終會輸出的文件,根據需要做的事選擇不同的stage, 這里選擇PROCESS_ASSETS_STAGE_ANALYSE的原因是,需要處理index.htm中的中文,所以需要選擇一個非常靠后的鉤子,其他鉤子參考 (相關文檔)

2. 讀取代碼中所有的中文

首先需要寫一個函數,用于匹配相鄰的中文字符,如,源碼中含有<p>失聯</p><div>系鞋帶</div>, 返回:['失聯', '系鞋帶']。將返回的字符數組,以分隔符分隔,如['失聯', '系鞋帶'] => 失聯'-'系鞋帶' , 分隔的原因:如中文簡體 => 中文繁體(存在多形字):失聯系鞋帶 => 失聯繫鞋帶, 而正確的結果應該是 失聯系鞋帶失聯是一個詞組,系鞋帶是一個詞組,轉換后不會有變化的, 而聯系在一起的時候就會變成 聯繫

/***?@description?返回中文詞組數組, 如:?<p>你好</p><div>世界</div>, ?返回:?['你好', '世界']*?@param?{*}?content?打包后的bundle文件內容*?@returns*/
function?getLanguageList(content,?regex)?{let?index?=?0,termList?=?[],term?=?'',list;?//?遍歷獲取到的中文數組while?((list?=?regex.exec(content)))?{if?(list.index?!==?index?+?1?&&?term)?{termList.push(term);term?=?'';}term?+=?list[0];index?=?list.index;}if?(term?!==?'')?{termList.push(term);}return?termList;
}

在以上代碼TODO: 的位置繼續編寫, 獲取所有chunk中的中文并保存至chunkAllList數組中

let?chunkAllList?=?[];
//?先將所有的chunk中的`指定字符詞組`存起來
for?(const?[pathname,?source]?of?Object.entries(assets))?{//?只讀取js與html文件中的中文,其他的文件不需要if?(!(pathname.endsWith('js')?||?pathname.endsWith('.html')))?{continue;}//?獲取當前chunk的源代碼字符串let?chunkSourceCode?=?source.source();//?獲取chunk中所有的中文。const?chunkSourceLanguageList?=?getLanguageList(chunkSourceCode,?regex);//?如果小于0,說明當前文件中沒有?`指定字符詞組`,不需要替換if?(chunkSourceLanguageList.length?<=?0)?continue;chunkAllList.push({//?原文本數組chunkSourceLanguageList,// separator為分割符默認為:?-chunkSourceLanguageStr:?chunkSourceLanguageList.join(separator),//?chunk原代碼chunkSourceCode,//?chunk的輸出路徑pathname,});
}

3. 請求翻譯API, 獲得翻譯后的結果

因為有些chunk中中文是很少的, 比如一個chunk中只有2個字,另一個chunk中只有3個字,那么就沒必要請求兩次翻譯接口,為了減少請求次數,先將所有chunk中的中文合成一個字符串,并用_分隔開用于區分是屬于那個chunk中的內容。

const?chunkAllSourceLanguageStr?=?chunkAllList
.map((item)?=>?item.chunkSourceLanguageStr).join(`_`);

合成一個字符串以后,還需要進行切割,因為一次最大支持翻譯5000個字符

//?合理的分割所有chunk中讀取的字符,供谷歌API翻譯,不能超過谷歌翻譯的限制
const?sourceList?=?this.getSourceList(chunkAllSourceLanguageStr,?limit);
getSourceList(sourceStr,?limit)?{let?len?=?sourceStr.length;let?index?=?0;if?(limit)?{}const?chunkSplitLimitList?=?[];while?(len?>?0)?{let?end?=?index?+?limit;const?str?=?sourceStr.slice(index,?end);chunkSplitLimitList.push(str);index?=?end;len?=?len?-?limit;}return?chunkSplitLimitList;
}

切割完成后,最后使用Promise.all去請求所有的接口,所有的翻譯成功才能算成功

//?翻譯
const?tempTargetList?=?await?Promise.all(sourceList.map(async?(text)?=>?{return?await?transform({translateApiUrl:?translateApiUrl,text:?text,from:?from,to:?to,});})
);

4. 將翻譯后的結果寫入至代碼中

得到了所有chunk中的中文簡體翻譯后的繁體,最后遍歷chunk數組chunkAllList,將源代碼中的

for?(let?i?=?0;?i?<?chunkAllList.length;?i++)?{const?{chunkSourceLanguageStr,chunkSourceLanguageList,pathname,chunkSourceCode,}?=?chunkAllList[i];let?sourceCode?=?chunkSourceCode;//?將簡體轉換為繁體targetList[i].split(separator).forEach((phrase,?index)?=>?{sourceCode?=?sourceCode.replace(chunkSourceLanguageList[index],phrase);});//?if?(outputTxt)?{writeContent?+=?this.writeFormat(pathname,chunkSourceLanguageStr,targetList[i]);}compilation.updateAsset(pathname,?new?sources.RawSource(sourceCode));
}

以上代碼為不完全代碼,完整代碼及插件使用方式請參考:translate-language-webpack-plugin

5. 輸出對照文本

如下:主要是輸出每個chunk中的中文用于對照,如果說頁面沒有其它動態的文字,且這些文字需要應用特殊的字體,也可以使用這些讀取出來的字打包一個字體文件,比一整個字體文件小很多很多。

b6e82a4c2d07b6b4f85d83c6adec3d6e.png
image.png

7總結

注意:會將頁面上包括js中的中文全部替換,但是接口返回的文字是無法轉換的,由后端返回對應繁體

至此一個完整的業務需求就已經優化的七七八八了,翻譯插件理論上支持任意語言互轉,但是由于翻譯的語義不同,往往翻譯出來的意思不是我們想要的,適用于簡體繁體互轉。


f4044a8d5fae03b576347d717f69161c.gif

·················?若川簡介?·················

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。

29eb118a5fd36f996079063a41508113.png

識別方二維碼加我微信、拉你進源碼共讀

今日話題

略。分享、收藏、點贊、在看我的文章就是對我最大的支持~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/274836.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274836.shtml
英文地址,請注明出處:http://en.pswp.cn/news/274836.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

全球 化 化_全球化設計

全球 化 化重點 (Top highlight)Designing for a global audience can feel daunting. Do you localize your product? Or, do you internationalize your product? And what does that even entail?為全球觀眾設計可能會令人生畏。 您是否將產品本地化&#xff1f; 還是您將…

springMVC_數據的處理過程

1、DispatcherServlet&#xff1a;作為前端控制器&#xff0c;負責分發客戶的請求到 Controller 其在web.xml中的配置如下&#xff1a; <servlet><servlet-name>dispatcherServlert</servlet-name><servlet-class>org.springframework.web.servlet.Dis…

面試體驗:Facebook 篇(轉)

http://www.cnblogs.com/cathsfz/archive/2012/11/05/facebook-interview-experience.html 2012-11-05 08:20 by Cat Chen, 23266閱讀, 121評論, 收藏, 編輯 Google、Microsoft 和 Yahoo 都是去年的事情了&#xff0c;接下來說說今年…

JavaScript 新增兩個原始數據類型

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列JavaS…

axure低保真原型_如何在Google表格中創建低保真原型

axure低保真原型Google Sheets is a spreadsheet, just like Microsoft Excel.Google表格是一個電子表格&#xff0c;就像Microsoft Excel一樣。 Most people associate it with calculating numbers. But Google Sheets is actually great for organizing your ideas, making…

Weblogic EJB 學習筆記(3)精

編輯實體bean的高級課程 1. 怎樣開發主健類 ejb的主健類主要用做持久存儲和ejb容器中的唯一標識符. 通常主健類的字段直接映射到數據庫中的主健字段. 如果主健只是由單個實體bean字段組成.且其數據類型是基本的java類.如string,則bean作者不必開發自定義的主健類. 只需要在配置…

Lerna 運行流程剖析

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列Lerna…

手動創建線程池 效果會更好_創建更好的,可訪問的焦點效果

手動創建線程池 效果會更好Most browsers has their own default, outline style for the :focus psuedo-class.大多數瀏覽器對于&#xff1a;focus psuedo-class具有其默認的輪廓樣式。 Chrome’s default outline styleChrome瀏覽器的默認輪廓樣式 This outline style is cr…

C++builder enum類型

C/C code #pragmaoption push -b-enumTThreadPriority { tpIdle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest, tpTimeCritical }; //這是字節型的.理論上說這是可能的最小整形.可以是1Byte, 2Bytes, 4Bytes...#pragmaoption pop#pragmaoption push -benumTThreadPriori…

chrome瀏覽器世界之窗瀏覽器的收藏夾在哪?

今天心血來潮&#xff0c;用一個查重軟件刪除重復文件&#xff0c;結果把chrome瀏覽器和世界之窗瀏覽器的收藏夾給刪除了&#xff0c;導致我保存的好多網頁都沒有了&#xff0c;在瀏覽器本身和網上都沒有找到這兩個瀏覽器默認的收藏夾在哪個位置&#xff0c;只好用DiskGenius 把…

Vue3究竟好在哪里 等推薦

話不多說&#xff0c;這一次花了幾小時精心為大家挑選了30余篇好文&#xff0c;供大家閱讀學習&#xff0c;提升自己的技術視野以及擴展自己的知識儲備。本文閱讀技巧&#xff0c;先粗看標題&#xff0c;感興趣可以都關注一波&#xff0c;一起共同進步。前端從進階到入院框架原…

eazy ui 復選框單選_UI備忘單:單選按鈕,復選框和其他選擇器

eazy ui 復選框單選重點 (Top highlight)Pick me! Pick me! No, pick me! In today’s cheat sheet we will be looking at selectors and how they differ. Unlike most of my other cheat sheets, this will focus on two components (radio buttons and checkboxes) side by…

過濾詞

<?xml version"1.0" encoding"GB2312"?>-<wordList> <word>,</word> <word>.</word> <word><</word> <word>></word> <word>?</word> <word>/</word> <…

VS2010 VC Project的default Include設置

http://blog.csdn.net/jeffchen/article/details/5491435 VS2010與以往的版本一個最大的不同是&#xff1a;VC Directory設置的位置和以前的版本不一樣。VS2010之前&#xff0c;VC Directory的設置都是在IDE的Tools->Options中設置的&#xff1b;VS2010改為&#xff0c;分別…

初級中級高級_初級職位,(半)高級職位

初級中級高級As a recent hire at my new job, as expected, a lot of things seemed scary and overwhelming. The scariest part was not the unfamiliarity with certain tasks or certain tools, but in communicating with higher-level coworkers, managers and bosses. …

如何寫好技術文章(看張鑫旭老師的直播總結

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan12 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列本文是…

Fact Table and Dimension Table In My Opinion

23轉載于:https://www.cnblogs.com/answeryou/archive/2012/05/10/2495122.html

iOS 流媒體 基本使用 和方法注意

項目里面需要添加視頻方法 我自定義 選用的是 avplayer 沒選擇 MediaPlayer 原因很簡單 , avplayer 會更容易擴展 有篇博客 也很好地說明了 使用avplayer的優越性 blog.csdn.net/think12/article/details/8549438在iOS開發上&#xff0c;如果遇到需要播放影片&#xff0c;…

figma下載_遷移至Figma

figma下載Being an intuitive and user-friendly tool and having the possibility of real-time collaboration are some of the main reasons people choose to use Figma. But the migration process to Figma may sometimes be painful or time-consuming. 人們選擇使用Fig…

metaWeblog 相關的參數

Function: Creates a new post on your blog. tags如果沒會自動那一個 但是categroies如果與已經建立的不同,就會忽略掉的 可以用 string.replace(\n,).split(,) Parameters: Blog ID – For use in multisite installations, typically 0 for single sites Username – WordPr…