ionic 3 開發(fā)環(huán)境切換

ionic 3 開發(fā)環(huán)境切換

解決的問題

  1. 項目開發(fā)時,瀏覽器調(diào)試存在的 CORS(跨域)問題
  2. 編譯iosandroid時,(半)自動化切換環(huán)境的問題
  3. 根據(jù)不同的環(huán)境,使用不同的key(如果Google Maps key
  4. Android打包簽名的key

開始

項目假設有3中環(huán)境:

  1. dev,開發(fā)者本機開發(fā)環(huán)境
  2. uat,用戶測試環(huán)境
  3. prod,生產(chǎn)(正式)環(huán)境

src/environments/下創(chuàng)建對應文件:

.
├── README.md
├── config.xml
├── ionic.config.json
├── package-lock.json
├── package.json
├── src
│   ├── environments
│   │   ├── environment.dev.ts  // dev
│   │   ├── environment.prod.ts //prod
│   │   ├── environment.ts
│   │   └── environment.uat.ts  // uat
│   ├── index.html
│   ├── manifest.json
│   ├── pages
│   ├── service-worker.js
│   └── theme
│       └── variables.scss
├── tsconfig.json
├── tslint.json
└── yarn.lock

environment.ts說起

environment.ts:

export const Environment = {
  mode: 'dev',
  debug: true,
  baseUrl: 'http://localhost:8080/dev/',
  endpoint: '/ionic',
  defaultOnly: true,
  cordova: {
    name: 'Developer app.',
    id: 'io.ionic.dev',
    version: '1.0.1'
  },
  metadata: { // this propertie will be removed when build.
    googleMapKeyAndroid: 'dev-google-maps-key-android',
    googleMapKeyIOS: 'dev-google-maps-key-ios'
  }
}

environment.ts文件是其它environment.*.ts的基類,編譯時,會把environment.*.ts覆蓋到environment.ts中。這樣做的好處是,environment.*.ts可以選擇性的重寫自己需要的屬性,而不是所有都必須重寫。

比如

假設所有的環(huán)境下,debug都為true,那對應的environment.ts

export const Environment = {
  debug: true,
}

其它的environment.*.ts中,如environment.uat.ts接可以省略debug: true.

其它環(huán)境

environment.dev.ts:

export const Environment = {
}

environment.uat.ts:

export const Environment = {
  mode: 'uat',
  baseUrl: 'http://localhost:8080/uat/',
  cordova: {
    name: 'UAT app.',
    id: 'io.ionic.uat'
  },
  metadata: {
    googleMapKeyAndroid: 'uat-google-maps-key-android',
    googleMapKeyIOS: 'uat-google-maps-key-ios'
  }
}

environment.prod.ts:

export const Environment = {
  mode: 'prod',
  debug: false,
  baseUrl: 'https://ionicframework.com/',
  cordova: {
    name: 'Prod app.',
    id: 'io.ionic.prod'
  },
  metadata: {
    googleMapKeyAndroid: 'prod-google-maps-key-android',
    googleMapKeyIOS: 'prod-google-maps-key-ios'
  }
}

實現(xiàn)(半)自動化切換環(huán)境

要實現(xiàn)環(huán)境切換,需要的步驟如下:

  1. 編寫js腳本,在執(zhí)行編譯命令時,讀取、整合environment.*.ts配置,生成整合后的臨時文件environment.tmp.
  2. ionic的編譯配置指向臨時文件environment.tmp.
  3. 編譯

在編寫腳本之前,需要將嵌入腳本:

修改tsconfig.json

插入:

{
  "compilerOptions": {
    "paths": {
      "@app/env": [
        "environments/environment"
      ]
    }
  }
}

修改package.json

插入:

"config": {
    "ionic_webpack": "./scripts/webpack.config.js"
  }

編寫webpack.config.js腳本

創(chuàng)建目錄及文件scripts/webpack.config.js

.
├── README.md
├── config.xml
├── ionic.config.json
├── package-lock.json
├── package.json
├── scripts
│   ├── config-env.js
│   ├── envconfig-writer.js
│   ├── environment-reader.js
│   ├── hooks
│   ├── proxy-set.js
│   └── webpack.config.js
├── src
│   ├── app
│   ├── assets
│   ├── environments
│   ├── index.html
│   ├── manifest.json
│   ├── pages
│   ├── service-worker.js
│   └── theme
├── tsconfig.json
├── tslint.json
└── yarn.lock

編寫代碼:

const chalk = require("chalk");
const fs = require('fs');
const _fs = require('fs-extra');
const path = require('path');
const useDefaultConfig = require('@ionic/app-scripts/config/webpack.config.js');

var argv = require('minimist')(process.argv.slice(2));
var env = process.env.ENV_MODE ? process.env.ENV_MODE : 'dev'; // Set default env='dev'
var release = argv.release ? true : false;

const envReader = require('./environment-reader');

let envConfigData = envReader(env);
let envConfigDataCpy = JSON.parse(JSON.stringify(envConfigData));
if (envConfigDataCpy.metadata) {
  delete envConfigDataCpy.metadata;
}
let envConfigDataStr = JSON.stringify(envConfigDataCpy);
let pathEnv = path.resolve(path.join('.', 'src', 'environments', 'environment.tmp'));
fs.writeFileSync(pathEnv, `export const Environment = ${envConfigDataStr}`);

useDefaultConfig.dev.resolve.alias = {
  "@app/env": pathEnv
};
useDefaultConfig.prod.resolve.alias = {
  "@app/env": pathEnv
};
module.exports = function () {
  return useDefaultConfig;
};

因為需要加載environment.*.ts的內(nèi)容,所以還需要一個工具類environment-reader.js:
environment-reader.js

require('typescript-require');
const chalk = require('chalk');
const fs = require('fs');
const path = require('path');
const objectAssignDeep = require(`object-assign-deep`);

module.exports = function (envMode) {
  if (typeof envMode === "undefined") {
    console.error(chalk.red(` \n [Error] missing env \n `));
    process.exit(-1);
  }
  let dirEnv = path.resolve('src', 'environments');
  if (!fs.existsSync(dirEnv)) {
    console.error(chalk.red(`${dirEnv} not exist! \n `));
    process.exit(-1);
  }
  let fileEnv = path.join(dirEnv, `environment` + `.${envMode}.ts`);
  if (!fs.existsSync(fileEnv)) {
    console.error(chalk.red(`${fileEnv} not found! \n `));
    process.exit(-1);
  }

  let fileDefaltEnv = path.join(dirEnv, 'environment.ts');
  let envDefaut = require(fileDefaltEnv).Environment;
  let envConfig = require(fileEnv).Environment;

  if (!envDefaut) {
    console.error(chalk.red(`${fileDefaltEnv} invaild.\n`));
    process.exit(-1);
  }
  if (!envConfig) {
    console.error(chalk.red(`${fileEnv} invaild.\n`));
    process.exit(-1);
  }
  return objectAssignDeep(copy(envDefaut), envConfig);

  // return Object.assign(copy(envDefaut), envConfig);
}

/**
 * Copy Object
 * @param {*} params object
 * @returns Copy
 */
function copy(params) {
  return JSON.parse(JSON.stringify(params));
}

測試

home.ts中:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Environment as ENV } from '@app/env'   // import 編寫的環(huán)境

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  constructor(public navCtrl: NavController) {
    let e = ENV;
    console.log(ENV.mode);
    debugger;
  }

}

在控制臺中執(zhí)行:

ENV_MODE=dev ionic serve

設置Proxy代理

執(zhí)行完上面的操作后,可以完成自動化切換環(huán)境,但還沒解決在瀏覽器中調(diào)試時的CORS 跨域問題,這里我們可以通過ionic自帶的代理完成.
創(chuàng)建文件scripts/proxy-set.js

.
├── README.md
├── config.xml
├── ionic.config.json
├── package-lock.json
├── package.json
├── scripts
│   ├── config-env.js
│   ├── envconfig-writer.js
│   ├── environment-reader.js
│   ├── hooks
│   ├── proxy-set.js
│   └── webpack.config.js
├── tsconfig.json
├── tslint.json
└── yarn.lock

proxy-set.js

const envMode = process.env.ENV_MODE;
if (typeof envMode === 'undefined') {
  return;
}
const envReader = require('./environment-reader');
const chalk = require("chalk");
const fs = require('fs');
const path = require('path');

let envConfigData = envReader(envMode);

setProxy(envConfigData);

/**
 * Set serve's proxy
 * @param {ionic.config.JSON} configData
 */
function setProxy(configData) {
  let fileConfig = path.resolve('ionic.config.json');
  if (!fs.existsSync(fileConfig)) {
    return;
  }
  let config = JSON.parse(fs.readFileSync(fileConfig, 'utf-8'));
  config.proxies = [];
  config.proxies.push({
    'proxyUrl': configData.baseUrl,
    'path': configData.endpoint
  });
  console.log(chalk.blue(`Save config to ${fileConfig} \n `));
  fs.writeFileSync(fileConfig, JSON.stringify(config));
}

package.json中,加入ionic CLIHooks
package.json

"scripts": {
    "ionic:serve:before": "node ./scripts/proxy-set.js"
}

測試

執(zhí)行

ENV_MODE=dev ionic serve

注意輸出:

> ionic-app-scripts serve --address 0.0.0.0 --port 8100 --livereload-port 35729 --dev-logger-port 53703 --nobrowser
[app-scripts] [11:07:50]  ionic-app-scripts 3.2.0
[app-scripts] [11:07:50]  watch started ...
[app-scripts] [11:07:50]  build dev started ...
[app-scripts] [11:07:50]  Proxy added:/ionic => http://localhost:8080/dev/  // <= 注意這行,如果有輸出,代表成功
[app-scripts] [11:07:50]  clean started ...

自動修改Cordova platform編譯參數(shù)

在編譯Cordova時,我們需要修改config.xml中的id,name,version等屬性,這些屬性就是我們在前文的environment.*.tscordova中已經(jīng)定義的屬性,我們在編譯前讀取屬性,再替換就能實現(xiàn)我們要的效果

思路

這里可能有幾個case需要考慮(鑒于我們現(xiàn)在基本只要照顧ios&android,所以我們默認只有這兩個platform)。

  1. 我們需要通過一個臨時文件來保存當前的env的狀態(tài),否則可能在已添加android平臺的情況下,再添加ios會導致環(huán)境參數(shù)不一致的問題(如先添加devandroid,再添加ios時,由于錯誤添加了uat的版本)
  2. 有些Cordova plugin,如Google Maps,替換對應的key時,不是只修改package.json|config.xml下的參數(shù)就能正確實現(xiàn)切換的,只有remove plugin后重新添加plugin時才能正確切換
  3. etc...

實現(xiàn)自動修改config.xml

我們需要修改config.xml中的id、version、name三個參數(shù),所以,創(chuàng)建文件scripts/config-env.js

.
├── README.md
├── config.xml
├── ionic.config.json
├── package-lock.json
├── package.json
├── scripts
│   ├── config-env.js
│   ├── envconfig-writer.js
│   ├── environment-reader.js
│   ├── hooks
│   ├── proxy-set.js
│   └── webpack.config.js
├── tsconfig.json
├── tslint.json
└── yarn.lock

config-eng.js

const xml2js = require('xml2js');
const fs = require('fs-extra');
const chalk = require("chalk");
const path = require('path');
module.exports = function (ENV_MODE) {
  if (typeof ENV_MODE === "undefined") {
    console.error(chalk.red(" \n ENV_MODE was require!"));
    process.exit(-1);
  }

  const envReader = require('./environment-reader');
  let configData = envReader(ENV_MODE);

  let configFile = path.resolve('config.xml');

  //backup config.xml
  let bakFile = path.resolve('config.xml.bak');
  if (!fs.existsSync(bakFile)) {
    console.log("Copy config.xml to config.xml.bak");
    fs.copySync(configFile, bakFile);
  }
  let xmlData = fs.readFileSync(configFile, "utf-8");
  let config = parseStringSync(xmlData);
  config.widget.$.id = configData.cordova.id;
  config.widget.$.version = configData.cordova.version;
  config.widget.name = [configData.cordova.name];

  //Save to file.json --> xml
  let builder = new xml2js.Builder();
  let jsonxml = builder.buildObject(config);
  try {
    fs.writeFileSync(configFile, jsonxml);
  } catch (error) {
    console.error(`Throw exception when write: ${configFile}`);
    console.error(error);
    process.exit(-1);
  }
}

function parseStringSync(xmlData) {
  let result;
  new xml2js.Parser().parseString(xmlData, (e, r) => {
    result = r;
  });
  return result;
}

一個Bug

做完上面那一步,已經(jīng)可以實現(xiàn)了。但是,前文中提到的case,還沒有解決,所以我們還需要解決它
創(chuàng)建scripts/before_platform_add.js
before_platform_add.js

var chalk = require('chalk');
var _fs = require('fs-extra')

module.exports = function (ctx) {
  var fs = ctx.requireCordovaModule('fs'),
    path = ctx.requireCordovaModule('path'),
    deferral = ctx.requireCordovaModule('q').defer();
  let platformPath = path.join(ctx.opts.projectRoot, "platforms");
  let envMode = process.env.ENV_MODE;
  let envData;

  let envConfigFile = path.resolve('src', 'environments', 'platform.env.json.tmp');
  const envWriter = require('../envconfig-writer');

  if (fs.existsSync(envConfigFile)) { // Exist,compare input and file's envMode.
    envData = JSON.parse(fs.readFileSync(envConfigFile, 'utf-8'));
    if ((typeof envMode === "undefined" && typeof envData.mode === "undefined") || typeof envData.mode === "undefined") {
      console.error(chalk.red(` \n [Error] missing --env or ${envConfigFile} propertie 'mode' was undefined.`));
      process.exit(-1);
    }
    if (typeof envMode === "undefined") {
      envMode = envData.mode; //when missing '--env'
    } else if (envMode !== envData.mode) { // overwrite file && remove platforms dir.
      _fs.removeSync(platformPath); // Remove platforms
      if (!fs.existsSync(platformPath)) { // check
        fs.mkdirSync(platformPath);
      }
      envWriter(envMode); // Save
    }
  } else {
    if (typeof envMode === 'undefined') { // default env.
      envMode = 'dev';
    }
    envWriter(envMode);
  }

  console.log(chalk.yellow(`\n Environment: \t ${envMode} \n `));

}

config.xml中插入

<widget id="io.ionic.dev version=”1.0.1">
    
    <hook src="scripts/hooks/before_platform_add.js" type="before_platform_add" />
    
</widget>

測試

執(zhí)行

 ENV_MODE=dev ionic cordova build android

查看config.xml,成功

<widget id="io.ionic.dev version=”1.0.1">
    <name> Developer app.</name>
    <!--... -->
</widget>

依據(jù)環(huán)境切換第三方服務的Key[Google Maps為例]

在前面創(chuàng)建的webpack.config.js中,有這么一段代碼:

if (process.env.IONIC_PLATFORM) { // Try to build cordova native app
  require('./config-env')(envConfigData.mode); // Save config to config.xml
  let glob = require('glob'),
    path = require('path');

  glob.sync(path.resolve('scripts', 'plugins') + '/*.js').forEach((file) => {
    try {
      console.log(`Find plugin: ${file}.`);
      require(path.resolve(file))(path, fs, JSON.parse(JSON.stringify(envConfigData)));
    } catch (error) {
      console.error(chalk.red(` \n [Error] require(${file}) \n  \t ${error}`));
    }
  });

  processPlatform();
}

這是掃描scripts/plugins文件夾下的js腳本,并運行。這會在ionic cordova build {platform}時調(diào)用
我們創(chuàng)建scripts/pluigns/replace_googlemaps_key.js

.
├── README.md
├── config.xml
├── ionic.config.json
├── package-lock.json
├── package.json
├── scripts
│   ├── config-env.js
│   ├── envconfig-writer.js
│   ├── environment-reader.js
│   ├── hooks
│   ├── proxy-set.js
│   └── webpack.config.js
├── tsconfig.json
├── tslint.json
└── yarn.lock

replace_googlemaps_key.js

const xml2js = require('xml2js');

module.exports = function (path, fs, envData) {
  console.log('\n Replace Google Maps Key...');
  let fileFetch = path.resolve('plugins', 'fetch.json');
  if (fs.existsSync(fileFetch)) {
    let fetchJson = JSON.parse(fs.readFileSync(fileFetch, 'utf-8'));
    let mapsPlugin = fetchJson['cordova-plugin-googlemaps'];
    if (mapsPlugin) {
      mapsPlugin.variables.API_KEY_FOR_ANDROID = envData.metadata.googleMapKeyAndroid;
      mapsPlugin.variables.API_KEY_FOR_IOS = envData.metadata.googleMapKeyIOS;
      fs.writeFileSync(fileFetch, JSON.stringify(fetchJson));
      console.log(`[Google Maps Key] save config: ${fileFetch}`);
    }
  }
  let filePackage = path.resolve('package.json');
  let packageData = JSON.parse(fs.readFileSync(filePackage, 'utf-8'));
  try {
    let mapsPlugin = packageData.cordova.plugins['cordova-plugin-googlemaps'];
    mapsPlugin.API_KEY_FOR_ANDROID = envData.metadata.googleMapKeyAndroid;
    mapsPlugin.API_KEY_FOR_IOS = envData.metadata.googleMapKeyIOS;
    fs.writeFileSync(filePackage, JSON.stringify(packageData));
    console.log(`[Google Maps Key] save package.json: ${filePackage} \n`);
  } catch (error) {
    console.log(error);
  }

  let fileConfig = path.resolve('config.xml');
  let configXml = fs.readFileSync(fileConfig, 'utf-8');
  let configData = parseStringSync(configXml);
  configData.widget.plugin.forEach(plugin => {
    if (plugin.$.name === 'cordova-plugin-googlemaps') {
      plugin.variable.forEach(_var => {
        if (_var.$.name === 'API_KEY_FOR_ANDROID') {
          _var.$.value = envData.metadata.googleMapKeyAndroid;
        } else if (_var.$.name === 'API_KEY_FOR_IOS') {
          _var.$.value = envData.metadata.googleMapKeyIOS;
        }
      });
    }
  });
  //Save to file.json --> xml
  let builder = new xml2js.Builder();
  let jsonxml = builder.buildObject(configData);
  try {
    fs.writeFileSync(fileConfig, jsonxml);
  } catch (error) {
    console.error(`Throw exception when write: ${fileConfig}`);
    console.error(error);
    process.exit(-1);
  }
}

function parseStringSync(xmlData) {
  let result;
  new xml2js.Parser().parseString(xmlData, (e, r) => {
    result = r;
  });
  return result;
}

完成

Android 簽名

簽名利用的是Cordova Hooks實現(xiàn)

  1. 創(chuàng)建debug-keys.jksrelease-keys.jks兩個證書文件,并寫好對應的*.properties文件,放至etc/sign/目錄下

    .
    ├── README.md
    ├── config.xml
    ├── etc
    │   └── sign
    │       ├── debug-keys.jks
    │       ├── debug-signing.properties
    │       ├── release-keys.jks
    │       └── release-signing.properties
    ├── ionic.config.json
    ├── package-lock.json
    ├── package.json
    ├── tsconfig.json
    ├── tslint.json
    └── yarn.lock
    
  2. 編寫對應的*.properties文件
    debug-signing.propertie:

    storeFile=debug-keys.jks
    storePassword=debugdebug
    keyAlias=debug
    keyPassword=debugdebug
    
  3. 創(chuàng)建scripts/hooks/move_android_keys.js
    move_android_keys.js

    var chalk = require('chalk');
    var _fs = require('fs-extra')
    
    module.exports = function (ctx) {
      var fs = ctx.requireCordovaModule('fs'),
        path = ctx.requireCordovaModule('path'),
        deferral = ctx.requireCordovaModule('q').defer();
    
      let pathSign = path.resolve('etc', 'sign');
      let pathAndroid = path.resolve('platforms', 'android');
    
      let files = _fs.readdirSync(pathSign);
      console.log(chalk.blue(`Copy ${pathSign} ====> ${pathAndroid}`));
      files.forEach(file => {
        _fs.copyFileSync(path.join(pathSign, file), path.join(pathAndroid, file));
      });
    }
    
  4. config.xml中插入

    <widget>
        <platform name="android">
            <hook src="scripts/hooks/move_android_keys.js" type="before_compile" />
        </platform>
    </widget>
    

測試

執(zhí)行(測試release簽名)

ENV_MODE=dev ionic cordova build android --release

成功

:app:packageRelease UP-TO-DATE
:app:assembleRelease
:app:cdvBuildRelease

BUILD SUCCESSFUL in 4s
48 actionable tasks: 2 executed, 46 up-to-date

修改的文件

.
├── config.xml
├── etc
│   └── sign
│       ├── debug-keys.jks
│       ├── debug-signing.properties
│       ├── release-keys.jks
│       └── release-signing.properties
├── ionic.config.json
├── package.json
├── scripts
│   ├── config-env.js
│   ├── envconfig-writer.js
│   ├── environment-reader.js
│   ├── hooks
│   │   ├── before_platform_add.js
│   │   └── move_android_keys.js
│   ├── plugins
│   │   └── replace_googlemaps_key.js
│   ├── proxy-set.js
│   └── webpack.config.js
├── src
│   └── environments
│       ├── environment.dev.ts
│       ├── environment.prod.ts
│       ├── environment.ts
│       └── environment.uat.ts
└── tsconfig.json

Github

log2c/ionic-multi-environment

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容