<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          前端賦能業(yè)務(wù) - Node實(shí)現(xiàn)自動(dòng)化部署平臺(tái)

          共 13576字,需瀏覽 28分鐘

           ·

          2020-09-26 21:03

          授權(quán)轉(zhuǎn)載自:yacan8

          https://github.com/yacan8/blog/issues/27

          前言

          是否有很多人跟我一樣有這樣的一個(gè)煩惱,每天有寫不完的需求、改不完的BUG,每天擼著重復(fù)、繁瑣的業(yè)務(wù)代碼,擔(dān)心著自己的技術(shù)成長(zhǎng)。

          其實(shí)換個(gè)角度,我們所學(xué)的所有前端技術(shù)都是服務(wù)于業(yè)務(wù)的,那我們?yōu)槭裁床幌朕k法使用前端技術(shù)為業(yè)務(wù)做點(diǎn)東西?這樣既能解決業(yè)務(wù)的困擾,也能讓自己擺脫每天只能寫重復(fù)繁瑣代碼的困擾。

          本文主要為筆者針對(duì)當(dāng)前團(tuán)隊(duì)內(nèi)的一些業(yè)務(wù)問題,實(shí)現(xiàn)的一個(gè)自動(dòng)化部署平臺(tái)的技術(shù)方案。

          背景

          去年年初,由于團(tuán)隊(duì)里沒有前端,剛好我是被招過來的第一個(gè),也是唯一一個(gè)FE,于是我接手了一個(gè)一直由后端維護(hù)的JSSDK項(xiàng)目,其實(shí)也說不上項(xiàng)目,接手的時(shí)候它只是一個(gè)2000多行代碼的胖腳本,沒有任何工程化痕跡。

          業(yè)務(wù)需求

          這個(gè)JSSDK,主要作用是在后端了為業(yè)務(wù)方分配appKey之后,前端將appKey寫死在JSSDK中,上傳到CDN后,為業(yè)務(wù)方提供數(shù)據(jù)采集服務(wù)的腳本。

          有的同學(xué)可能有疑問,為什么不像一些正常的SDK一樣,appKey是以參數(shù)的形式傳入到JSSDK中,這樣就可以統(tǒng)一所有業(yè)務(wù)方使用同一個(gè)JSSDK,而不需要為每個(gè)業(yè)務(wù)業(yè)務(wù)方都提供一個(gè)JSSDK。其實(shí)我剛開始也是這么想的,于是我向我的leader提出了我的這個(gè)想法,被拒絕了,拒絕原因如下:

          • appKey如果以參數(shù)形式傳入,對(duì)業(yè)務(wù)方的接入成本有所增加,會(huì)出現(xiàn)appKey填錯(cuò)的問題。
          • 業(yè)務(wù)方接入JSSDK之后,希望每次JSSDK版本迭代對(duì)業(yè)務(wù)方來說是無感知的(也就是版本迭代是覆蓋式發(fā)布),如果所有業(yè)務(wù)方使用同一個(gè)JSSDK,每次JSSDK的版本迭代,一次發(fā)版會(huì)一次性對(duì)所有業(yè)務(wù)方都有影響,會(huì)增加風(fēng)險(xiǎn)。

          由于我的leader現(xiàn)在主要是負(fù)責(zé)產(chǎn)品推廣,經(jīng)常和業(yè)務(wù)方打交道,可能他更能站在業(yè)務(wù)方的角度來考慮問題。所以,我的leader選擇犧牲項(xiàng)目的維護(hù)成本來降低SDK的接入成本和規(guī)避風(fēng)險(xiǎn),可以理解。

          那既然我們改變不了現(xiàn)狀,那就只能適應(yīng)現(xiàn)狀。

          項(xiàng)目痛點(diǎn)

          那么針對(duì)原來沒有任何工程化情況的胖腳本,每次新增一個(gè)業(yè)務(wù)方,我需要做的事情如下:

          1. 打開一個(gè)胖腳本和JSSDK接入文檔,拷貝一份新的。
          2. 找后端要分配好的appKey,找對(duì)對(duì)應(yīng)的appKey那一行代碼手動(dòng)修改。
          3. 手動(dòng)混淆修改完好的腳本并上傳到CDN。
          4. 修改JSSDK接入文檔中CDN的地址,保存后發(fā)送給業(yè)務(wù)方。

          整個(gè)過程都需要手動(dòng)進(jìn)行,相對(duì)來說非常繁瑣,并且一不小心就會(huì)填錯(cuò),每次都需要對(duì)腳本和接入文檔進(jìn)行檢查。

          針對(duì)以上情況,得到我們需要解決的問題:

          • 怎樣針對(duì)一個(gè)新的業(yè)務(wù)方快速輸出一份新的JSSDK和接入文檔?
          • 怎樣快速對(duì)新的JSSDK進(jìn)行混淆并上傳到CDN。

          自動(dòng)化方案

          介紹方案之前,先上一張平臺(tái)截圖,以便先有一個(gè)直觀的認(rèn)識(shí):

          SDK自動(dòng)化部署平臺(tái)主要實(shí)現(xiàn)了JSSDK的編譯,發(fā)布測(cè)試(在線預(yù)覽),上傳CDN功能。

          服務(wù)端技術(shù)棧包括:

          • 框架 Express
          • 熱更新 nodemon
          • 依賴注入 awilix
          • 數(shù)據(jù)持久化 sequelize
          • 部署 pm2

          客戶端技術(shù)棧就不介紹了,Vue全家桶 + vue-property-decorator + vuex-class。

          項(xiàng)目搭建參考:

          Vue+Express+Mysql 全棧初體驗(yàn)

          https://juejin.im/post/5ce96694f265da1bc5523f69

          自動(dòng)化部署平臺(tái)主要依賴于 GIT + 本地環(huán)境 + 私有NPM源 + MYSQL,各環(huán)節(jié)之間進(jìn)行通信交互,完成自動(dòng)化部署。

          主要達(dá)到的效果:本地環(huán)境拉取git倉(cāng)庫代碼后,進(jìn)行需求開發(fā),完成后發(fā)布一個(gè)帶Rollup的SDK編譯器包到私有NPM倉(cāng)庫,自動(dòng)化部署平臺(tái)在工程目錄安裝指定版本的SDK,并且備份到本地,在SDK編譯時(shí),選擇特定版本的Rollup的SDK編譯器,并傳參(如appKey,appId等)到編譯器中進(jìn)行編譯,同時(shí)自動(dòng)生成JSSDK接入文檔等后打包成帶描述文件的Release包,在上傳到CDN時(shí),將描述文件的對(duì)應(yīng)的信息寫入MYSQL中進(jìn)行保存。

          版本管理

          由于JSSDK原本只是一個(gè)腳本,我們必須實(shí)現(xiàn)項(xiàng)目的工程化,從而完成版本管理,方便快速版本切換進(jìn)行發(fā)布,回滾,進(jìn)而快速止損。

          首先,我們需要將項(xiàng)目工程化,使用Rollup進(jìn)行模塊管理,并且在發(fā)包NPM包的時(shí)候,輸入為各種參數(shù)(如appKey)輸出為一個(gè)Rollup Complier的函數(shù),然后使用rollup-plugin-replace在編譯時(shí)候替換代碼中具體的參數(shù)。

          lib/build.js,JSSDK中發(fā)包的入口文件,提供給SDK編譯時(shí)使用

          import?*?as?rollup?from?'rollup';
          const?replace?=?require('rollup-plugin-replace');
          const?path?=?require('path');
          const?pkgPath?=?path.join(__dirname,?'..',?'package.json');
          const?pkg?=?require(pkgPath);
          const?proConfig?=?require('./proConfig');

          function?getRollupConfig(replaceParams)?{
          ????const?config?=?proConfig;
          ????//?注入系統(tǒng)變量
          ????const?replacePlugin?=?replace({
          ????????'__JS_SDK_VERSION__':?JSON.stringify(pkg.version),
          ????????'__SUPPLY_ID__':?JSON.stringify(replaceParams.supplyId?||?'7102'),
          ????????'__APP_KEY__':?JSON.stringify(replaceParams.appKey)
          ????});
          ????return?{
          ????????input:?config.input,
          ????????output:?config.output,
          ????????plugins:?[
          ????????????...config.plugins,
          ????????????replacePlugin
          ????????]
          ????};
          };

          module.exports?=?async?function?(params)?{
          ????const?config?=?getRollupConfig({
          ????????supplyId:?params.supplyId?||?'7102',
          ????????appKey:?params.appKey
          ????});
          ????const?{
          ????????input,
          ????????plugins
          ????}?=?config;
          ????const?bundle?=?await?rollup.rollup({
          ????????input,
          ????????plugins
          ????});
          ????const?compiler?=?{
          ????????async?write(file)?{
          ????????????await?bundle.write({
          ????????????????file,
          ????????????????format:?'iife',
          ????????????????sourcemap:?false,
          ????????????????strict:?false
          ????????????});
          ????????}
          ????};
          ????return?compiler;
          };

          在自動(dòng)化部署平臺(tái)中,使用shelljs安裝JSSDK包:

          import?{route,?POST}?from?'awilix-express';
          import?{Api}?from?'../framework/Api';
          import?*?as?shell?from?'shell';
          import?*?as?path?from?'path';

          @route('/supply')
          export?default?class?SupplyAPI?extends?Api?{
          ????//?some?code

          ????@route('/installSdkVersion')
          ????@POST()
          ????async?installSdkVersion(req,?res)?{
          ????????const?{version}?=?req.body;
          ????????const?pkg?=?`@baidu/xxx-js-sdk@${version}`;
          ????????const?registry?=?'http://registry.npm.baidu-int.com';
          ????????shell.exec(`npm?i?${pkg}?--registry=${registry}`,?(code,?stdout,?stderr)??=>?{
          ????????????if?(code?!==?0)?{
          ????????????????console.error(stderr);
          ????????????????res.failPrint('npm?install?fail');
          ????????????????return;
          ????????????}
          ????????????//?sdk包備份路徑
          ????????????const?sdkBackupPath?=?this.sdkBackupPath;
          ????????????const?sdkPath?=?path.resolve(sdkBackupPath,?version);
          ????????????shell.mkdir('-p',?sdkPath).then((code,?stdout,?stderr)?=>?{
          ????????????????if?(code?!==?0)?{
          ????????????????????console.error(stderr);
          ????????????????????res.failPrint(`mkdir?\`${sdkPath}\`?error.`);
          ????????????????????return;
          ????????????????}
          ????????????????const?modulePath?=?path.resolve(process.cwd(),?'node_modules',?'@baidu',?'xxx-js-sdk');
          ????????????????//?拷貝安裝后的文件,方便后續(xù)使用
          ????????????????shell.cp('-rf',?modulePath?+?'/.',?sdkPath).then((code,?stdout,?stderr)?=>?{
          ????????????????????if?(code?!==?0)?{
          ????????????????????????console.error(stderr);
          ????????????????????????res.failPrint(`backup?sdk?error.`);
          ????????????????????????return;
          ????????????????????}
          ????????????????????res.successPrint(`${pkg}?install?success.`);
          ????????????????});
          ????????????})
          ????????});
          ????}
          }

          Release包

          Release包就是我們?cè)谏蟼鞯紺DN之前需要準(zhǔn)備的壓縮包。因此,打包JSSDK之后,我們需要生成的文件有,接入文檔、JSSDK DEMO預(yù)覽頁面、JSSDK編譯結(jié)果、描述文件。

          首先,打包函數(shù)如下:

          import?{Service}?from?'../framework';
          import?*?as?fs?from?'fs';
          import?path?from?'path';
          import?_?from?'lodash';

          export?default?class?SupplyService?extends?Service?{
          ????async?generateFile(supplyId,?sdkVersion)?{
          ????????//?數(shù)據(jù)庫查詢對(duì)應(yīng)的業(yè)務(wù)方的CDN文件名
          ????????const?[sdkInfoErr,?sdkInfo]?=?await?this.supplyDao.getSupplyInfo(supplyId);
          ????????if?(sdkInfoErr)?{
          ????????????return?this.fail('服務(wù)器錯(cuò)誤',?null,?sdkInfoErr);
          ????????}
          ????????const?{appKey,?cdnFilename,?name}?=?sdkInfo;
          ????????//?需要替換的數(shù)據(jù)
          ????????const?data?=?{
          ????????????name,
          ????????????supplyId,
          ????????????appKey,
          ????????????'sdk_url':?`https://***.com/sdk/${cdnFilename}`
          ????????};
          ????????try?{
          ????????????//?編譯JSSDK
          ????????????const?sdkResult?=?await?this.buildSdk(supplyId,?appKey,?sdkVersion);
          ????????????//?生成接入文檔
          ????????????const?docResult?=?await?this.generateDocs(data);
          ????????????//?生成預(yù)覽DEMO?html文件
          ????????????const?demoHtmlResult?=?await?this.generateDemoHtml(data,?'sdk-demo.html',?`JSSDK-接入頁面-${data.name}.html`);
          ????????????//?生成release包描述文件
          ????????????const?sdkInfoFileResult?=?await?this.writeSdkVersionFile(supplyId,?appKey,?sdkVersion);
          ????????????
          ????????????const?success?=?docResult?&&?demoHtmlResult?&&?sdkInfoFileResult?&&?sdkResult;
          ????????????if?(success)?{
          ????????????????//?release目標(biāo)目錄
          ????????????????const?dir?=?path.join(this.releasePath,?supplyId?+?'');
          ????????????????const?fileName?=?`${supplyId}-${sdkVersion}.zip`;
          ????????????????const?zipFileName?=?path.join(dir,?fileName);
          ????????????????//?壓縮所有結(jié)果文件
          ????????????????const?zipResult?=?await?this.zipDirFile(dir,?zipFileName);
          ????????????????if?(!zipResult)?{
          ????????????????????return?this.fail('打包失敗');
          ????????????????}
          ????????????????//?返回壓縮包提供下載
          ????????????????return?this.success('打包成功',?{
          ????????????????????url:?`/${supplyId}/${fileName}`
          ????????????????});
          ????????????}?else?{
          ????????????????return?this.fail('打包失敗');
          ????????????}
          ????????}?catch?(e)?{
          ????????????return?this.fail('打包失敗',?null,?e);
          ????????}
          ????}
          }

          編譯JSSDK

          JSSDK的編譯很簡(jiǎn)單,只需要加載對(duì)應(yīng)版本的JSSDK的編譯函數(shù),然后將對(duì)應(yīng)的參數(shù)傳入編譯函數(shù)得到一個(gè)Rollup Compiler,然后將 Compiler 結(jié)果寫入Release路徑即可。

          export?default?class?SupplyService?extends?Service?{
          ????async?buildSdk(supplyId,?appKey,?sdkVersion)?{
          ????????try?{
          ????????????const?sdkBackupPath?=?this.sdkBackupPath;
          ????????????//?加載對(duì)應(yīng)版本的備份的JSSDK包的Rollup編譯函數(shù)
          ????????????const?compileSdk?=?require(path.resolve(sdkBackupPath,?sdkVersion,?'lib',?'build.js'));
          ????????????const?bundle?=?await?compileSdk({
          ????????????????supplyId,
          ????????????????appKey:?Number(sdkInfo.appKey)
          ????????????});
          ????????????const?releasePath?=?path.resolve(this.releasePath,?supplyId,?`${supplyId}-sdk.js`);
          ????????????//?Rollup?Compiler?編譯結(jié)果至release目錄
          ????????????await?bundle.write(releasePath);
          ????????????return?true;
          ????????}?catch?(e)?{
          ????????????console.error(e);
          ????????????return?false;
          ????????}
          ????}
          }

          生成接入文檔

          原理很簡(jiǎn)單,使用JSZip,打開接入文檔模板,然后使用Docxtemplater替換模板里的特殊字符,然后重新生成DOC文件:

          import?Docxtemplater?from?'docxtemplater';
          import?JSZip?from?'JSZip';

          export?default?class?SupplyService?extends?Service?{

          ????async?generateDocs(data)?{
          ????????return?new?Promise(async?(resolve,?reject)?=>?{
          ????????????if?(data)?{
          ????????????????//?讀取接入文檔,替換appKey,cdn路徑
          ????????????????const?supplyId?=?data.supplyId;
          ????????????????const?docsFileName?=?'sdk-doc.docx';
          ????????????????const?supplyFilesPath?=?path.resolve(process.cwd(),?'src/server/files');
          ????????????????const?content?=?fs.readFileSync(path.resolve(supplyFilesPath,?docsFileName),?'binary');
          ????????????????const?zip?=?new?JSZip(content);
          ????????????????const?doc?=?new?Docxtemplater();
          ????????????????//?替換`[[`前綴和`]]`后綴的內(nèi)容
          ????????????????doc.loadZip(zip).setOptions({delimiters:?{start:?'[[',?end:?']]'}});
          ????????????????doc.setData(data);
          ????????????????try?{
          ????????????????????doc.render();
          ????????????????}?catch?(error)?{
          ????????????????????console.error(error);
          ????????????????????reject(error);
          ????????????????}
          ????????????????//?生成DOC的buffer
          ????????????????const?buf?=?doc.getZip().generate({type:?'nodebuffer'});
          ????????????????const?releasePath?=?path.resolve(this.releasePath,?supplyId);
          ????????????????//?創(chuàng)建目標(biāo)目錄
          ????????????????shell.mkdir(releasePath).then((code,?stdout,?stderr)?=>?{
          ????????????????????if?(code?!==?0?)?{
          ????????????????????????resolve(false);
          ????????????????????????return;
          ????????????????????}
          ????????????????????//?將替換后的結(jié)果寫入release路徑
          ????????????????????fs.writeFileSync(path.resolve(releasePath,?`JSSDK-文檔-${data.name}.docx`),?buf);
          ????????????????????resolve(true);
          ????????????????}).catch(e?=>?{
          ????????????????????console.error(e);
          ????????????????????resolve(false);
          ????????????????});
          ????????????}
          ????????});
          ????}
          }

          生成預(yù)覽DEMO頁面

          與接入文檔生成原理類似,打開一個(gè)DEMO模板HTML文件,替換內(nèi)部字符,重新生成文件:

          export?default?class?SupplyService?extends?Service?{
          ????generateDemoHtml(data,?file,?toFile)?{
          ????????return?new?Promise((resolve,?reject)?=>?{
          ????????????const?supplyId?=?data.supplyId;
          ????????????//?需要替換的數(shù)據(jù)
          ????????????const?replaceData?=?data;
          ????????????//?打開文件
          ????????????const?content?=?fs.readFileSync(path.resolve(supplyFilesPath,?file),?'utf-8');
          ????????????//?字符串替換`{{`前綴和`}}`后綴的內(nèi)容
          ????????????const?replaceContent?=?content.replace(/{{(.*)}}/g,?(match,?key)?=>?{
          ????????????????return?replaceData[key]?||?match;
          ????????????});
          ????????????const?releasePath?=?path.resolve(this.releasePath,?supplyId);
          ????????????//?寫入文件
          ????????????fs.writeFile(path.resolve(releasePath,?toFile),?replaceContent,?err?=>?{
          ????????????????if?(err)?{
          ????????????????????console.error(err);
          ????????????????????resolve(false);
          ????????????????}?else?{
          ????????????????????resolve(true);
          ????????????????}
          ????????????});
          ????????});
          ????}
          }

          生成Release包描述文件

          將當(dāng)前打包的一些參數(shù)存在一個(gè)文件中的,一并打包到Release包中,作用很簡(jiǎn)單,用來描述當(dāng)前打包的一些參數(shù),方便上線CDN的時(shí)候記錄當(dāng)前上線的是哪個(gè)SDK版本等

          export?default?class?SupplyService?extends?Service?{
          ????async?writeSdkVersionFile(supplyId,?appKey,?sdkVersion)?{
          ????????return?new?Promise(resolve?=>?{
          ????????????const?writePath?=?path.resolve(this.releasePath,?supplyId,?'version.json');
          ????????????//?Release描述數(shù)據(jù)
          ????????????const?data?=?{version:?sdkVersion,?appKey,?supplyId};
          ????????????try?{
          ????????????????//?寫入release目錄
          ????????????????fs.writeFileSync(writePath,?JSON.stringify(data));
          ????????????????resolve(true);
          ????????????}?catch?(e)?{
          ????????????????console.error(e);
          ????????????????resolve(false);
          ????????????}
          ????????});
          ????}
          }

          打包所有文件結(jié)果

          將之前生成的JSSDK編譯結(jié)果、接入文檔、預(yù)覽DEMO頁面文件,描述文件使用archive打包起來:

          export?default?class?SupplyService?extends?Service?{
          ????zipDirFile(dir,?to)?{
          ????????return?new?Promise(async?(resolve,?reject)?=>?{
          ????????????const?output?=?fs.createWriteStream(to);
          ????????????const?archive?=?archiver('zip');
          ????????????archive.on('error',?err?=>?reject(err));
          ????????????archive.pipe(output);
          ????????????const?files?=?fs.readdirSync(dir);
          ????????????files.forEach(file?=>?{
          ????????????????const?filePath?=?path.resolve(dir,?file);
          ????????????????const?info?=?fs.statSync(filePath);
          ????????????????if?(!info.isDirectory())?{
          ????????????????????archive.append(fs.createReadStream(filePath),?{
          ????????????????????????'name':?file
          ????????????????????});
          ????????????????}
          ????????????});
          ????????????archive.finalize();
          ????????????resolve(true);
          ????????});
          ????}
          }

          CDN部署

          大部分上傳到CDN都為像CDN源站push文件,而正好我們運(yùn)維在我的自動(dòng)化部署平臺(tái)的機(jī)器上掛載了NFS,即我只需要本地將JSSDK文件拷貝到共享目錄,就實(shí)現(xiàn)了CDN文件上傳。

          export?default?class?SupplyService?extends?Service?{
          ????async?cp2CDN(supplyId,?fileName)?{
          ????????//?讀取描述文件
          ????????const?sdkInfoPath?=?path.resolve(this.releasePath,?''?+?supplyId,?'version.json');
          ????????if?(!fs.existsSync(sdkInfoPath))?{
          ????????????return?this.fail('Release描述文件丟失,請(qǐng)重新打包');
          ????????}
          ????????const?sdkInfo?=?JSON.parse(fs.readFileSync(sdkInfoPath,?'utf-8'));
          ????????sdkInfo.cdnFilename?=?fileName;
          ????????//?將文件拷貝至文件共享目錄
          ????????const?result?=?await?this.cpFile(supplyId,?fileName,?false);
          ????????//?上傳成功
          ????????if?(result)?{
          ????????????//?將Release包描述文件的數(shù)據(jù)同步到MYSQL
          ????????????const?[sdkInfoErr]?=?await?this.supplyDao.update(sdkInfo,?{where:?{supplyId}});
          ????????????if?(sdkInfoErr)?{
          ????????????????return?this.fail('JSSDK信息記錄失敗,請(qǐng)重試',?null,?jssdkInfoResult);
          ????????????}
          ????????????return?this.success('上傳成功',?{url})
          ????????}
          ????????return?this.fail('上傳失敗');
          ????}
          }

          項(xiàng)目成效

          項(xiàng)目效益還是很明顯,從本質(zhì)上解決了我們需要解決的問題:

          • 完成了項(xiàng)目的工程化,自動(dòng)化生成JSSDK和接入文檔。
          • 編譯過程中自動(dòng)化進(jìn)行混淆,并實(shí)現(xiàn)了一鍵上傳至CDN。

          節(jié)省了人工上傳粘貼代碼的時(shí)間,大大地提高了工作效率。

          這個(gè)項(xiàng)目還是19年前半年個(gè)人花業(yè)余時(shí)間完成的工具項(xiàng)目,后來得到了Leader的重視,將工具正式升級(jí)為平臺(tái),集成了很多業(yè)務(wù)相關(guān)的配置在平臺(tái),我19年的前半年KPI就這么來的,哈~~~

          總結(jié)

          或者這一套思路對(duì)每個(gè)業(yè)務(wù)都比較適用

          1. 了解業(yè)務(wù)的背景
          2. 發(fā)現(xiàn)業(yè)務(wù)的痛點(diǎn)
          3. 尋找解決方案并主動(dòng)推進(jìn)實(shí)現(xiàn)
          4. 解決問題

          其實(shí)每個(gè)項(xiàng)目中的痛點(diǎn)都一般都是XX的性能低下、XX非常低效,還是比較容易發(fā)現(xiàn)的,這個(gè)時(shí)候只需要主動(dòng)的尋找方案并推進(jìn)實(shí)現(xiàn)就OK了。

          前端技術(shù)離不開業(yè)務(wù),技術(shù)永遠(yuǎn)服務(wù)于業(yè)務(wù),離開了業(yè)務(wù)的技術(shù),那是完全沒有落腳點(diǎn)的技術(shù),完全沒有意義的技術(shù)。所以,除了寫寫頁面,利用前端頁面實(shí)現(xiàn)工具化、自動(dòng)化,從而推進(jìn)到平臺(tái)化也是一個(gè)不錯(cuò)的落腳點(diǎn)選擇。


          我是依揚(yáng),螞蟻集團(tuán)-保險(xiǎn)團(tuán)隊(duì)正在大量招聘中,詳情見:我們是螞蟻保險(xiǎn)前端團(tuán)隊(duì),我們今年在做什么,有興趣快來聯(lián)系我吧[email protected]

          》》面試官都在用的題庫,快來看看《

          瀏覽 111
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩精品久久久久久免费 | 亚洲三级先锋影音 | 天天爽天天澡天天爽视频 - 百度 无码毛片一区二区三区四区五区六区 | 中文字幕无码不卡免费视频 | 男女啪啪啪免费网站 |