前端本地化部署

前端本地化部署

http://zoo.zhengcaiyun.cn/blog/article/localized-deployment

前言

現在成熟的前端團隊裏面都有自己的內部構建平臺,我司雲長便是我們 CI/CD 的提效利器。我先來簡單介紹下我司的雲長,此雲長非彼雲長,雲長主要做的是:獲取部署的項目,分支,環境基本信息後開始拉取代碼,安裝依賴,打包,並且將項目的一些資源靜態文件上傳 CDN,再將生成的代碼再打包成鏡像文件,然後將這份鏡像上傳到鏡像倉庫後,最後調用 K8S 的鏡像部署服務,進行鏡像按環境的部署,這就是我們雲長做的事情。如果想從零開始搭建一個自己團隊的部署平臺可以看下我們往期文章 如何搭建適合自己團隊的構建部署平臺,本期我們只是針對雲長中靜態資源本地化的功能做細緻闡述。

場景分析

爲了網絡安全,客戶會要求我們的應用是要完全部署在內網的,那我們需要做什麼呢?第一我們需要考慮前端代碼中是不是有些直接訪問外網資源?第二是不是後端返回了靜態資源地址在某種情況下就訪問了?第三 CDN 資源具體有那些類型呢?前端直接訪問的 CDN 的資源太普遍了,如下既有 at.alicdn.com,又有我們自己內部的靜態資源 luban.zcycdn.com, sitecdn.zcycdn.com 等。如下這些就在我們代碼中使用的靜態資源地址。

 <link rel='stylesheet' href='//at.alicdn.com/t/fontnm.css' />
 <img src="https://sitecdn.zcycdn.com/f2e/8.png"alt="收貨人"/>
 <img src="https://luban.zcycdn.com/f2e/8.png"alt="收貨人"/>
 //css中字體文件
 src:url(https://sitecdn.zcycdn.com/t/font_148178j4i.eot);
 src:url(https://sitecdn.zcycdn.com/t/font1_4i.woff);

爲了保證我們內網中可以訪問我們討論出以下兩個方案

方案一

DNS 解析做轉發

我們通過 DNS 服務這一層去處理,具體 DNS 如何進行的二域名,三級域名進行解析,如何 DNS 緩存,以及什麼是 13 臺根服務器,我們這次不做深入探討,我們只需要 DNS 的可以進行域名解析,解析到指定的 IP 服務上即可。

那我們是不是可以想一下,是不是把代碼中訪問的靜態資源的域名攔截一下,DNS 解析成本地服務的地址是不是就可以了呢?爲了更清楚的理解,我做一個例子如下:

我們代碼中需要訪問某個圖片,CDN 地址:https://cdn.zcycdn.com/b/a.js

上傳提前把 a.js 這個文件提前放到本地服務器上訪問地址:https://demo.com/b/a.js

當代碼運行的時候,代碼中訪問了 https://cdn.zcycdn.com 的時候,DNS 直接地址解析成 https://demo.com 的 IP 地址,達到訪問靜態資源的目的

看起來這個蠻簡單的,不需要各個業務負責人排查修改自己代碼中的靜態資源,勝利在望了,興致沖沖的跑去找運維童鞋提議是不是可以這樣做,然而運維把我說的服服帖帖。運維童鞋說:靜態資源放在對象存儲或者服務器上,通過 IP 或者域名的方式都可以請求的到,不過 IP 只支持 HTTP 的方式,域名 + SSL 證書的方式支持 HTTPS,可以做一些加密,讓你的資源或者請求內容進行加密,不容易被破解,域名證書之前有 3 到 5 年的,3 年前已經改掉了,目前申請的證書都是一年的,那就預示着不僅僅要用戶配置我們提供的 DNS 規則,還要配合我們一年一更新證,想要客戶這樣配合那是不容易。如下圖所示:

DNS 只是幫我們把域名解析成了 IP, HTTPS 還需要證書驗證服務器身份,僅僅 DNS 攔截解析還不夠。模擬實現了一波大致思路:自己啓動一個靜態資源服務,以及 DNS 本地解析服務,當訪問 juejin.cn 域名的時候 IP 解析成本地的 IP 並且成功訪問到靜態資源,具體如下。

自己寫一個 DNS 服務

step1: 本地起一個服務

暫時存放靜態資源,模擬服務器上的資源

啓動服務訪問靜態資源

我們的目的:如果訪問 http://juejin.cn:3000/zcy.png (http://juejin.cn:3000/zcy.png) 的時候訪問到我們本地服務的靜態資源:http://10.201.45.121:3000/zcy.png (http://10.201.45.121:3000/zcy.png)

step2: 啓動一個本地 DNS 服務,攔截所有請求轉發到自己啓動的 IP  點擊查看源碼 (https://sitecdn.zcycdn.com/f2e-assets/7da606eb-d8fc-4a01-a633-fcfd60edc2c5.js)

step3:配置本地 DNS 解析

step4: 測試訪問 HTTP 和 HTTPS

訪問:http://juejin.cn:3000/zcy.png(http://juejin:3000/zcy.png)

如果是 https://juejin.cn:3000/zcy.png (https://juejin:3000/zcy.png)

如果訪問的是 HTTP 請求那就可以訪問,HTTPS 就不能訪問,側面證明了 HTTPS 的證書問題。HTTPS 對稱加密的祕鑰我們採用非對稱加密傳輸,數據傳輸還是使用對稱加密,這保證了數據加密傳輸,爲了保證防止冒充,CA(Certificate Authority), 頒發的證書就稱爲數字證書 (Digital Certificate),在非對稱加密階段,服務器會把證書會帶着非對稱加密的公鑰,一起返回,向瀏覽器證明服務器的身份 HTTPS 相比 HTTP 多了一層 SSL/TLS(安全層)如下圖。

方案二

項目在構建的時候掃描出項目中的靜態資源地址,從我們公網的 CDN 服務放到客戶自己的服務器上,修改源文件中的靜態資源地址爲客戶本地服務的訪問地址。

優缺點一目瞭然,方案一無需修改代碼,但是需要充分得到客戶的大力信任與支持需要配置 DNS 轉發,方案二無需勞煩客戶,即使後面有新增域名也不需要和客戶溝通,完全自己解決,但是對代碼有侵入性,會替換靜態資源的地址

我們通過以下 4 個階段拆解

統一封裝 runCommand 執行命令

function runCommand(cmd, args, options, before, end) {
  return new Promise((resolve, reject) ={
    log(before, blue)
    const spawn = childProcess.spawn(
      cmd,
      args,
      Object.assign(
        {
          cwd: global.WORKSPACE,
          stdio: 'inherit',
          shell: true,
        },
        options
      )
    )
    spawn.on('error'(error) ={
      log(error, chalk.red)
      reject(error)
    });
    spawn.on('close'(code) ={
      if (code !== 0) {
        return reject(`sh: ${cmd} ${args.join(' ')}`)
      }
      end && log(end, green)
      resolve()
    });
  })
}

1、pre 前置環境校驗

切換公司 nrm

 runCommand('nrm'['use''zcy-server']{}'switch nrm registry to zcy''switch nrm registry to zcy success')

下載依賴

runCommand('npm'['i''--unsafe-perm']{}'npm install''npm install success')

2、compile 編譯

不同環境需要上傳不同的地址因此需要動態修改 webpack 的 publicPath

const cdnConfigStr = `assetsPublicPath: 'http://dev.com',`
replaceFileContent(configPath, /assetsPublicPath:.+,/g, cdnConfigStr)
exports.replaceFileContent = function(filePath, source, target) {
  const fileContent = fs.readFileSync(filePath, 'utf-8')
  let targetFileContent = fileContent
  if (Array.isArray(source)) {
    source.forEach(([s, target]) ={
      if (target) {
        targetFileContent = targetFileContent.replace(s, target)
      }
    })
  } else {
    targetFileContent = fileContent.replace(source, target)
  }
  fs.writeFileSync(filePath, targetFileContent, 'utf-8')
}

編譯項目

 runCommand('npm'['run''build']{}`webpack build``webpack build success`)

3、靜態資源替換

替換 url 源碼地址

    const replaceWebpackDistContent = 
    async function(options = {},collectionAssets,folder) {
    const fileContent = fs.readFileSync(filePath, 'utf-8');
    let targetFileContent=fileContent;
    [
      [/(https\:)?\/\/g.alicdn.com\/[-a-zA-Z0-9@:%_\+.~#?&//=]+\.[-a-zA-Z0-9@:%_\+.~#?&//=]+/g, cdn],
      [/(https?\:)?\/\/sitecdn.zcycdn.com\/[-a-zA-Z0-9@:%_\+.~#?&//=]+\.[-a-zA-Z0-9@:%_\+.~#?&//=]+/g, cdn],
      [/(https\:)?\/\/cdn.zcycdn.com\/[-a-zA-Z0-9@:%_\+.~#?&//=]+\.[-a-zA-Z0-9@:%_\+.~#?&//=]+/g, cdn],
    ].forEach(([reg,uri])=>{
          targetFileContent=targetFileContent.replace(reg,function(match){
          let basename = '';
          let uriMath = match;
          basename = path.basename(uriMath);
          if(uriMath.slice(0,4)!='http'){ 
            uriMath='https:'+uriMath;
          }
          const parseUrl = url.parse(uriMath);
          
          collectionAssets({src:uriMath,fileName:path.basename(parseUrl.pathname)});
          console.log('🚀替換前',match);
          const myURL= new URL(projectName, uri);
          const replacedUrl = uri+'/'+projectName+parseUrl.path+(parseUrl.hash||'');
          console.log('🚀替換後', replacedUrl);
          return replacedUrl;
        })
    })
    fs.writeFileSync(filePath, targetFileContent, 'utf-8')
    }

獲取寫死在前端代碼中的靜態資源

  const downloadAssetsFiles= async function(img,forder){
  const staticAssets='staticAssets';
  let assetsUrl=getPwdPath(`${forder||''}${path.sep}${staticAssets}`);
  if(!fs.existsSync(assetsUrl)){
    fs.mkdirSync(assetsUrl);
  }
    return Promise.all(img.objUnique('src').map(({src,fileName})=>{
      if(fileName){
        return new Promise(function(resolve,reject){
          const originFileDir = path.join(assetsUrl,path.dirname(url.parse(src).pathname));
          fs.mkdirSync(originFileDir,{recursive:true});
          const uri = path.join(originFileDir,fileName); 
          download(uri,src,resolve,reject);
        }).catch(err=>{
          console.log(err)
          throw new Error(err);
        })
      }
        
    }))
    
}
function download(loadedUrl,src){
    const writeStream = fs.createWriteStream(loadedUrl);
    const readStream =  request(src);
    readStream.pipe(writeStream);
    readStream.on('end'function() {
      console.log(fileName,'文件下載成功');
    });
     writeStream.on("finish"function() {
      console.log(fileName,"文件寫入成功");
      writeStream.end();
    });
}
  downloadAssetsFiles(assetsArr,'dist');
  // 發現替換資源裏還有cdn,因此替換下載後的cdn裏面的cdn
  const assetsArr=[];
  await replaceWebpackDistContent(options,collectionAssets,'staticAssets');
  await downloadAssetsFiles(assetsArr,'dist');

4、OSS 推送靜態資源到客戶資源服務

const ossEndpoint = process.env.OSS_ENDPOINT;
const commonOptions = {
  accessKeyId: process.env.OSS_ACCESSKEYID ,
  accessKeySecret: process.env.OSS_ACCESSKEYSECRET,
  bucket: process.env.OSS_BUCKET,
  timeout: '120s',
}

const extraOptions = ossEndpoint
  ? {
    endpoint: ossEndpoint, // 從全局數據獲取,沒有會依賴 region
    cname: true,
  } : {
    region: process.env.OSS_REGION,
  }
const ossOptions = Object.assign({}, commonOptions, extraOptions);
const client = new OSS(ossOptions);
//onlinePath 訪問的文件地址
//curPath 上傳的文件地址
result = await client.put(onlinePath, curPath);

參考文檔

SSL/TLS 證書 1 年有效期新規 (https://www.trustasia.com/view-398-day-limit/)

node child_process 文檔 (https://link.juejin.cn/?target=http%3A%2F%2Fnodejs.cn%2Fapi%2Fchild_process.html%23child_process_child_process_fork_modulepath_args_options)

深入理解 Node.js 進程與線程 (https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fxgangzai%2Farticle%2Fdetails%2F98919412)

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/TT9sNgwgiuhQAucCmJWfRg