Electron升級(jí)更新服務(wù)
目錄
一、 背景 2
二、 目標(biāo) 2
三、 新插件的應(yīng)用 3
(一) vue-cli-plugin-electron-builder是什么 3
(二) 打包服務(wù)electron-builder的配置 5
(三) 更新服務(wù)electron-updater 10
(四) 應(yīng)用打包 18
(五) 測(cè)試更新 24一、 背景
很久前寫(xiě)了一篇《Electron自動(dòng)更新》介紹了當(dāng)時(shí)存在的問(wèn)題、自動(dòng)更新的方案、打包的兩種方式和開(kāi)發(fā)中存在的問(wèn)題。當(dāng)時(shí)文中應(yīng)用采用的是 squirrel.windows 的更新機(jī)制和 nsis 的自定義安裝策略。通過(guò) electron-builder 將兩者配置后,產(chǎn)出不同的安裝程序 setup.exe 和更新程序 nupkg。然后將 nsis 的 setup.exe 和 squirrel.windows 中的 nupkg 上傳到 electron-release-server 中。利用 electron-release-server 定時(shí)檢查策略,對(duì)比本地版本和線上版本,自動(dòng)下載依賴和程序,進(jìn)行更新并且替換,做到用戶無(wú)感知,操作不繁瑣。
二、 目標(biāo)
《Electron自動(dòng)更新》中的方案完成了當(dāng)時(shí)的目標(biāo)。
但是方案中的問(wèn)題很多。
問(wèn)題:
更新不受用戶控制,無(wú)法停止更新。沒(méi)法提示用戶是否更新。如果帶寬很小,應(yīng)用下載完成后就更新,打斷了正在執(zhí)行的腳本; 更新方案在linux環(huán)境中不適用; electron-vue項(xiàng)目作者很久不維護(hù); 官方提供的方案不詳細(xì),自己編寫(xiě)腳本偏多,考慮可能不全面。
現(xiàn)在的目標(biāo)是:
更為完善的自動(dòng)更新; 更新需要提示用戶,需要控制應(yīng)用是否更新; 申請(qǐng)管理員權(quán)限; 更新時(shí)加入loading。 這時(shí)候,新插件 「vue-cli-plugin-electron-builder」 就出世了。
三、 新插件的應(yīng)用
(一) vue-cli-plugin-electron-builder是什么
是一款構(gòu)建帶有 electron 桌面應(yīng)用的 vue.js 應(yīng)用程序的插件。
它的 github 地址:https://github.com/nklayman/vue-cli-plugin-electron-builder。
它的文檔:https://nklayman.github.io/vue-cli-plugin-electron-builder/。
具體配置,大家可以去查看文檔,寫(xiě)的很詳細(xì)。
優(yōu)點(diǎn):
一次編寫(xiě),到處可用 可以定制 支持測(cè)試和調(diào)試
飛鴻覺(jué)得這個(gè)框架很友好,不需要開(kāi)發(fā)者重寫(xiě)更新服務(wù),只需要管理好業(yè)務(wù)代碼并且提供靜態(tài)服務(wù)器用于更新,其他不需要開(kāi)發(fā)者操心,一步到位。
創(chuàng)建步驟:
先用vue-cli3或4創(chuàng)建vue項(xiàng)目 在項(xiàng)目中添加electron-builder依賴 啟動(dòng)應(yīng)用:npm run electron:serve 打包應(yīng)用:npm run electron:build
備注:當(dāng)然這些命令你可以在package.json中修改。
vue create electron-builder-demo 創(chuàng)建項(xiàng)目,
在項(xiàng)目中添加插件 vue add electron-builder 開(kāi)始下載 vue-cli-plugin-electron-builder

選擇 electron 版本,最好選擇最新的。

選擇后項(xiàng)目會(huì)去拉去electron,這個(gè)過(guò)程很漫長(zhǎng)。如果等不了,看看之前有沒(méi)有electron項(xiàng)目,將依賴復(fù)制過(guò)來(lái)。
npm run electron:serve啟動(dòng)應(yīng)用:

那么這個(gè)命令他做了什么?
啟動(dòng)內(nèi)置開(kāi)發(fā)服務(wù)器,并進(jìn)行一些修改配合electron正常工作; 捆綁主進(jìn)程; 啟動(dòng)electron應(yīng)用并告訴它加載上述開(kāi)發(fā)服務(wù)器的 url。
最后自動(dòng)打開(kāi)應(yīng)用界面:

你以為完了?不不不,還有很多步驟。
接下來(lái)我們要將打包配置和更新配置都要加上,才能將更新服務(wù)打通。
(二) 打包服務(wù)electron-builder的配置
此時(shí)還缺少打包配置,我們可以根據(jù) electron-builder文檔進(jìn)行配置。之所以不使用electron-packager,是因?yàn)檫@個(gè)插件是基于electron-builder開(kāi)發(fā)的。
其官網(wǎng)地址如下:https://www.electron.build/。
在根目錄下新增vue.config.js文件,填寫(xiě)配置。
vue.config.js是一個(gè)可選的配置文件,如果@vue/cli-service存在于項(xiàng)目根目錄package.json中,項(xiàng)目會(huì)自動(dòng)加載配置文件。
完整配置如下:
const path = require('path')
module.exports = {
// 部署應(yīng)用包時(shí)的基本 URL
publicPath: '/',
// 生成的生產(chǎn)環(huán)境構(gòu)建文件的目錄
outputDir: 'dist',
// 放置生成的靜態(tài)資源 (js、css、img、fonts) 的 (相對(duì)于 outputDir 的) 目錄
assetsDir: 'assets',
// 指定生成的 index.html 的輸出路徑 (相對(duì)于 outputDir)。也可以是一個(gè)絕對(duì)路徑。
indexPath: 'index.html',
// 文件名中包含hash
filenameHashing: true,
// 在 multi-page 模式下構(gòu)建應(yīng)用, 單頁(yè)面一般不需要考慮(詳情查看文檔配置)
pages: undefined,
// 保存時(shí)自動(dòng)觸發(fā)eslint
lintOnSave: process.env.NODE_ENV !== 'production',
// 是否使用包含運(yùn)行時(shí)編譯器的 Vue 構(gòu)建版本
runtimeCompiler: false,
// babel 顯示轉(zhuǎn)譯一個(gè)依賴
transpileDependencies: ['socket.io-client'],
// 生產(chǎn)環(huán)境source map 關(guān)閉可提升打包速度
productionSourceMap: false,
// crossorigin: undefined,
// integrity: false,
css: {
// modules: false,
requireModuleExtension: true,
extract: process.env.NODE_ENV === 'production',
sourceMap: false,
loaderOptions: {
less: {
prependData: ``
}
}
},
// 并行打包
parallel: true, // 默認(rèn)值require('os').cpus().length > 1,
pluginOptions: {},
// 本地開(kāi)發(fā)服務(wù)器配置
devServer: {
// 自動(dòng)打開(kāi)瀏覽器
open: true,
// 設(shè)置為0.0.0.0則所有的地址均能訪問(wèn)
host: '0.0.0.0',
port: 8888,
https: false,
hotOnly: false,
compress: true,
disableHostCheck: true,
// 使用代理
proxy: {
'/api': {
// 目標(biāo)代理服務(wù)器地址
target: 'http://10.66.194.44:8081/',
// 允許跨域
changeOrigin: true,
},
},
},
// 針對(duì)webpack的配置,如果遇到上述配置,能使用的盡量不要改動(dòng)webpack的配置
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
// 為生產(chǎn)環(huán)境修改配置...
config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true;
} else {
// 為開(kāi)發(fā)環(huán)境修改配置...
}
},
// chain模式下的webpack plugin配置
chainWebpack: config => {
// 使用svg-sprite-loader的vue.config配置 只應(yīng)用于src/icons目錄下
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule
.test(/\.svg$/)
.include.add(path.resolve(__dirname, './src/icons')).end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
const fileRule = config.module.rule('file')
fileRule.uses.clear()
fileRule
.test(/\.svg$/)
.exclude.add(path.resolve(__dirname, './src/icons'))
.end()
.use('file-loader')
.loader('file-loader')
config
.plugin('env')
.use(require.resolve('webpack/lib/ProvidePlugin'), [{
jQuery: 'jquery',
$: 'jquery',
"windows.jQuery": "jquery"
}]);
config.resolve.alias.set('@', path.join(__dirname, './src'))
},
pluginOptions: {
electronBuilder: {
externals: ['log4js'],
// If you are using Yarn Workspaces, you may have multiple node_modules folders
// List them all here so that VCP Electron Builder can find them
nodeModulesPath: ['./node_modules'],
nodeIntegration: true,
chainWebpackMainProcess: (config) => {
// 修復(fù)HMR
config.resolve.symlinks(true);
config.resolve.alias.set('@', path.join(__dirname, './src'))
// Chain webpack config for electron main process only
},
chainWebpackRendererProcess: (config) => {
// 修復(fù)HMR
config.resolve.symlinks(true);
// 使用svg-sprite-loader的vue.config配置 只應(yīng)用于src/icons目錄下
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule
.test(/\.svg$/)
.include.add(path.resolve(__dirname, './src/icons')).end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
const fileRule = config.module.rule('file')
fileRule.uses.clear()
fileRule
.test(/\.svg$/)
.exclude.add(path.resolve(__dirname, './src/icons'))
.end()
.use('file-loader')
.loader('file-loader')
config.resolve.alias.set('@', path.join(__dirname, './src'))
// Chain webpack config for electron renderer process only (won't be applied to web builds)
},
// Changing the Output Directory
outputDir: "dist_electron",
// Electron's Junk Terminal Output https://nklayman.github.io/vue-cli-plugin-electron-builder/guide/configuration.html#electron-s-junk-terminal-output
removeElectronJunk: false,
// Use this to change the entrypoint of your app's main process
mainProcessFile: 'background/main.js',
// Use this to change the entry point of your app's render process. default src/[main|index].[js|ts]
rendererProcessFile: 'src/main.js',
// Provide an array of files that, when changed, will recompile the main process and restart Electron
// Your main process file will be added by default
mainProcessWatch: ['background/main.js'],
// Provide a list of arguments that Electron will be launched with during "electron:serve",
// which can be accessed from the main process (src/background.js).
// Note that it is ignored when --debug flag is used with "electron:serve", as you must launch Electron yourself
// Command line args (excluding --debug, --dashboard, and --headless) are passed to Electron as well
// mainProcessArgs: ['--arg-name', 'arg-value']
builderOptions: {
"productName": "Vue Electron",
"appId": "com.VueElectron",
"publish": [{
"provider": "generic",
"url": "http://localhost:7777/dist_electron/"
}],
"win": {
"target": [
"nsis"
],
"icon": "./public/favicon.ico",
"requestedExecutionLevel": "highestAvailable"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./public/favicon.ico",
"uninstallerIcon": "./public/favicon.ico",
"installerHeaderIcon": "./public/favicon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"perMachine": false,
"unicode": true,
"deleteAppDataOnUninstall": false
}
}
}
}
}
electron-builder 配置可以在 「pluginOptions」 配置,也可以通過(guò)外部配置導(dǎo)入的方式。
(三) 更新服務(wù)electron-updater
當(dāng)然,要想要更新的話,需要加上更新插件。
之前飛鴻是使用 electron 自帶的 autoUpdater 插件,現(xiàn)在根據(jù)文檔建議,轉(zhuǎn)投 electron-updater 的 autoUpdater 懷抱了。
electron-updater 跟 electron 內(nèi)置的 autoUpdater 還是有區(qū)別的:
只需要靜態(tài)服務(wù)器存放更新文件和版本文件,不需要專用的更新版本服務(wù)器,后者使用的就是electron-release-server服務(wù)器,優(yōu)缺點(diǎn)很明顯,這里我就不做比較了,可以參考飛鴻之前寫(xiě)的《Electron自動(dòng)更新》; 代碼簽名驗(yàn)證不僅適用于macOS,也適用于Windows; 所有必須更新包都會(huì)自動(dòng)生成并發(fā)布; 支持下載進(jìn)度。
Windows 平臺(tái)是按照 nsis 更新,mac 是按照 DMG 更新,linux 是按照 AppImage 更新。
事件區(qū)別
| 事件 | 作用 | Electron自帶的autoUpdater | electron-updater |
|---|---|---|---|
| Checking-for-update | 開(kāi)始檢查更新的時(shí)候觸發(fā) | 無(wú)法差異 | 無(wú)法差異 |
| Update-available | 又可以使用的更新的時(shí)候觸發(fā)更新。 | 自動(dòng)下載,不可以阻斷 | 默認(rèn)自動(dòng)下載,可以阻斷轉(zhuǎn)化為手動(dòng)。如果autoDownload為true,則會(huì)自動(dòng)下載更新。提供 info |
| Update-not-available | 沒(méi)有可以使用的更新的時(shí)候觸發(fā) | 提供 info | |
| Update-downloaded | 更新下載完成的時(shí)候觸發(fā) | 更新包下載完成后可以阻斷自動(dòng)更新。但是應(yīng)用退出后會(huì)自動(dòng)更新。 | 更新包下載完成后可以阻斷。如果autoInstallOnAppQuit為true,則應(yīng)用退出后自動(dòng)更新。為false,則應(yīng)用退出后不會(huì)更新。 |
| Before-quit-for-update | 調(diào)用quitAndInstall()方法之后觸發(fā) | 無(wú) | |
| Error | 當(dāng)更新遇到錯(cuò)誤的時(shí)候觸發(fā) | 提供error信息 | 提供error信息 |
| Download-progress | 無(wú) | 顯示更新進(jìn)度,提供progress 進(jìn)度信息 bytesPerSecond percent total transferred |
方法區(qū)別
| 方法 | 作用 | Electron自帶的autoUpdater | electron-updater |
|---|---|---|---|
| autoUpdater.setFeedURL(url) | 設(shè)置檢查更新用的url,還初始化自動(dòng)更新 | 提供url、headers、serverType(適用于mac) | 跟electron-builder配置的發(fā)布配置選項(xiàng)強(qiáng)相關(guān),如provider、package、repo、owner等 |
| autoUpdater.getFeedURL() | 獲得當(dāng)前更新的url地址 | ||
| autoUpdater.checkForUpdates() | 詢問(wèn)服務(wù)器是否有更新 | 輪詢時(shí)間可以開(kāi)發(fā)自己設(shè)置 | 輪詢時(shí)間可以開(kāi)發(fā)自己設(shè)置 |
| autoUpdater.quitAndInstall() | 重啟應(yīng)用并在下載后安裝更新。只能在update-download事件后被調(diào)用 | 調(diào)用這個(gè)方法將首先關(guān)閉應(yīng)用,并在關(guān)閉后自動(dòng)調(diào)用app.quit()。執(zhí)行一次自動(dòng)更新可以不調(diào)用這個(gè)方法。但是在下一次打開(kāi)應(yīng)用的時(shí)候,應(yīng)用檢測(cè)到更新包下載完成后會(huì)自動(dòng)更新。 | 提供選項(xiàng)isSilent和isForceRunAfter isSilent默認(rèn)false,windows平臺(tái)應(yīng)用按照靜默模式安裝程序。isForceRunAfter即使是靜默安裝,也可以在完成后運(yùn)行應(yīng)用程序。 |
| autoUpdater.checkForUpdatesAndNotify() | 無(wú) | 詢問(wèn)服務(wù)器是否有更新,并且下載提示用戶 | |
| autoUpdater.downloadUpdate() | 無(wú) | 如果autoDownload選項(xiàng)設(shè)置為false,就可以使用這個(gè)方法手動(dòng)下載更新。 | |
| autoUpdater.channel() | 無(wú) | 更換自動(dòng)更新的channel,可以參考https://www.electron.build/tutorials/release-using-channels#release_using_channels |
?靜默模式安裝
??安裝時(shí)無(wú)需任何用戶干預(yù),直接按默認(rèn)設(shè)置安裝。就是更新的時(shí)候,應(yīng)用不需要重新讓用戶選擇安裝路徑等操作,應(yīng)用直接讀取之前的配置,按照第一次的安裝配置安裝,不顯示任務(wù)配置選項(xiàng)。
?
注意事項(xiàng):
靜默模式安裝需要相同數(shù)量的臨時(shí)磁盤空間,并使用與標(biāo)準(zhǔn)安裝相同的臨時(shí)存儲(chǔ)目錄。如果臨時(shí)目錄中沒(méi)有足夠的空間,安裝程序不會(huì)提醒用戶。 靜默模式安裝需要與標(biāo)準(zhǔn)安裝相同的時(shí)間。在靜默模式安裝開(kāi)始時(shí),會(huì)短暫顯示初始安裝程序窗口或消息,指示安裝已啟動(dòng)。沒(méi)有消息顯示,表明安裝正在進(jìn)行或已成功完成。
類型配置選項(xiàng)
electron-updater獨(dú)有的類型配置選項(xiàng)
| 參數(shù) | 作用 | 備注 |
|---|---|---|
| autoDownload | 是否在找到更新時(shí)自動(dòng)下載更新。默認(rèn)為true | 這個(gè)需要和autoUpdater.downloadUpdate()搭配使用 |
| autoInstallOnAppQuit | 是否在應(yīng)用退出時(shí)自動(dòng)安裝下載的更新默認(rèn)為true | 這個(gè)和autoUpdater.quitAndInstall()方法有關(guān) |
| allowPrerelease 是 | 否允許更新到預(yù)發(fā)布版本默認(rèn)為false | 這個(gè)和allowDowngrade有關(guān)。只支持github |
| fullChangelog | 獲取所有發(fā)行說(shuō)明(從當(dāng)前版本到最新版本),而不僅僅是最新版本。默認(rèn)為false | 只支持github |
| allowDowngrade | 只支持github 默認(rèn)為false | |
| channel | 獲取更新channel | 不支持github |
| requestHeaders | 請(qǐng)求頭 | |
| logger | 日志 | |
| signals | 為了類型安全,我們可以使用singals | |
| currentVersion | 當(dāng)前應(yīng)用程序版本信息 |
這樣看來(lái),electron-updater基本上兼容了原生autoUpdater,而且提供的東西功能更多更有用。現(xiàn)在,飛鴻肯定是選擇electron-updater。相信大家肯定也是這樣的。
安裝使用
下載electron-updater依賴
npm install --save-dev electron-updater
使用依賴包
const {
autoUpdater
} = require("electron-updater");
Vue-cli-plugin-electron-builder 的更新,也是提供好幾個(gè)方案,如 github 這類的第三方托管平臺(tái),參考更新例子:https://github.com/nklayman/electron-auto-update-example。
還有比如 minio 這類私有平臺(tái),參考更新例子:https://github.com/iffy/electron-updater-example。
在electron-builder配置的publish中加入url參數(shù),指向minio指定的項(xiàng)目桶。當(dāng)然我們的應(yīng)用是可以區(qū)分環(huán)境的。
builderOptions: {
……
"publish": [{
"provider": "generic",
"url": process.env.VUE_APP_PUBLISHMINIO
}],
……
}
在主進(jìn)程中,加入更新服務(wù)
……
autoUpdater.autoInstallOnAppQuit = false
autoUpdater.on('checking-for-update', () => {
logger.info('正在檢查更新……')
})
autoUpdater.on('update-available', (ev, info) => {
logger.info('下載更新包成功')
})
autoUpdater.on('update-not-available', (ev, info) => {
logger.info('現(xiàn)在使用的就是最新版本,不用更新')
})
autoUpdater.on('error', (ev, err) => {
logger.info('檢查更新出錯(cuò)')
logger.info(ev)
logger.info(err)
})
autoUpdater.on('download-progress', (ev, progressObj) => {
logger.info('正在下載...')
})
autoUpdater.on('update-downloaded', (ev, releaseNotes, releaseName) => {
logger.info('下載完成,更新開(kāi)始')
// Wait 5 seconds, then quit and install
// In your application, you don't need to wait 5 seconds.
// You could call autoUpdater.quitAndInstall(); immediately
const options = {
type: 'info',
buttons: ['確定', '取消'],
title: '應(yīng)用更新',
message: process.platform === 'win32' ? releaseNotes : releaseName,
detail: '發(fā)現(xiàn)有新版本,是否更新?'
}
dialog.showMessageBox(options).then(returnVal => {
if (returnVal.response === 0) {
logger.info('開(kāi)始更新')
setTimeout(() => {
autoUpdater.quitAndInstall()
}, 5000);
} else {
logger.info('取消更新')
return
}
})
});
……
開(kāi)頭需要導(dǎo)入 autoUpdater 和 dialog 等方法
import {
autoUpdater
} from 'electron-updater'
import {
app,
protocol,
BrowserWindow,
dialog
} from 'electron'
最后需要加入更新檢測(cè),什么時(shí)候加入檢測(cè),看業(yè)務(wù)的需求,飛鴻在應(yīng)用啟動(dòng)的時(shí)候加入檢測(cè)。
app.on('ready', async () => {
createWindow()
autoUpdater.checkForUpdates()
})
在這里飛鴻將更新提示使用electron提供的dialog寫(xiě)的,其實(shí)功能和checkForUpdatesAndNotify()一樣。
(四) 應(yīng)用打包
打包生成兩個(gè)不同版本號(hào)的應(yīng)用進(jìn)行檢測(cè)更新。
執(zhí)行命令:
npm run electron:build
那它做了什么呢?
先用webpack打包渲染進(jìn)程,打包出來(lái)的產(chǎn)物放在dist_electron,這個(gè)地址可以在配置中的outputDir中修改; 打包生成chunk-vendors.xxx.js、app.xxx.js和app.xxx.css放在其中的bundled\assets中; 渲染進(jìn)程構(gòu)建完成后,緊跟著構(gòu)建主進(jìn)程,捆綁后臺(tái)文件,還是打包進(jìn)dist_electron/bundled下,生成background.js; 最后用electron-builder構(gòu)建app,將web應(yīng)用程序代碼構(gòu)建成electron提供的桌面程序; 生成配置文件builder-effective-config.yaml; 最后用nsis構(gòu)建應(yīng)用。
electron-builder的配置:

builder-effective-config.yaml 就是 electron-builder 的配置項(xiàng)文件。

0.1.0版本打包成功:


雙擊打開(kāi)應(yīng)用



點(diǎn)擊完成,自動(dòng)運(yùn)行應(yīng)用

又能看到應(yīng)用界面了

桌面自動(dòng)生成快捷方式

Windows開(kāi)始菜單也會(huì)出現(xiàn)應(yīng)用

(五) 測(cè)試更新
接下來(lái)就是打包一個(gè)高版本的應(yīng)用來(lái)測(cè)試更新是否生效。
當(dāng)前版本是0.1.0,我們來(lái)打包一個(gè)0.2.0的吧。
修改一下package.json中的version,最后重新打包。
打包0.2.0:

dist_electron目錄下就會(huì)出現(xiàn)兩個(gè)安裝包

應(yīng)用是通過(guò)打包配置publish中的url提供的地址請(qǐng)求,檢測(cè)更新。
根據(jù)electron-builder的配置:

需要啟動(dòng)一個(gè)端口為7777的靜態(tài)文件服務(wù)器。
python -m SimpleHTTPServer 7777
重啟0.1.0,打開(kāi)就檢測(cè)到有新版本:

點(diǎn)擊取消,應(yīng)用不更新,用戶可以進(jìn)行這個(gè)版本的繼續(xù)操作。
查看日志

重啟0.1.0應(yīng)用,再次出現(xiàn)應(yīng)用提示界面,點(diǎn)擊確定,應(yīng)用消失,緊接著回到應(yīng)用安裝界面,當(dāng)然可以選擇靜默安裝。

然后下一步安裝,最后啟動(dòng)應(yīng)用,就更新到了0.2.0。

再打開(kāi)日志:

其中靜態(tài)服務(wù)器是用 python 起的,大家可以采用其他方式。
應(yīng)用先拿取 latest.yml 文件,比較其中的哈希碼。
如果和本地不同,再去拉取最新的安裝包進(jìn)行安裝。
應(yīng)用和功能、桌面快捷方式、Windows開(kāi)始菜單的應(yīng)用從0.1.0都變成了0.2.0。

最后,希望大家一定要點(diǎn)贊三連。
可以閱讀我的其他文章,見(jiàn)blog地址
一個(gè)學(xué)習(xí)編程技術(shù)的公眾號(hào)。每周推送高質(zhì)量的優(yōu)秀博文、開(kāi)源項(xiàng)目、實(shí)用工具、面試技巧、編程學(xué)習(xí)資源等等。目標(biāo)是做到個(gè)人技術(shù)與公眾號(hào)一起成長(zhǎng)。歡迎大家關(guān)注,一起進(jìn)步,走向全棧大佬的修煉之路
