ionic开发ionic开发ionic

ionic 3 开发环境切换

2018-11-19  本文已影响9人  凌音同学

ionic 3 开发环境切换

解决的问题

  1. 项目开发时,浏览器调试存在的 CORS(跨域)问题
  2. 编译iosandroid时,(半)自动化切换环境的问题
  3. 根据不同的环境,使用不同的key(如果Google Maps key
  4. Android打包签名的key

开始

项目假设有3中环境:

  1. dev,开发者本机开发环境
  2. uat,用户测试环境
  3. prod,生产(正式)环境

src/environments/下创建对应文件:

.
├── 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可以选择性的重写自己需要的属性,而不是所有都必须重写。

比如

假设所有的环境下,debug都为true,那对应的environment.ts

export const Environment = {
  debug: true,
}

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

其它环境

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'
  }
}

实现(半)自动化切换环境

要实现环境切换,需要的步骤如下:

  1. 编写js脚本,在执行编译命令时,读取、整合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脚本

创建目录及文件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的内容,所以还需要一个工具类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 编写的环境

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

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

}

在控制台中执行:

ENV_MODE=dev ionic serve

设置Proxy代理

执行完上面的操作后,可以完成自动化切换环境,但还没解决在浏览器中调试时的CORS 跨域问题,这里我们可以通过ionic自带的代理完成.
创建文件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"
}

测试

执行

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编译参数

在编译Cordova时,我们需要修改config.xml中的id,name,version等属性,这些属性就是我们在前文的environment.*.tscordova中已经定义的属性,我们在编译前读取属性,再替换就能实现我们要的效果

思路

这里可能有几个case需要考虑(鉴于我们现在基本只要照顾ios&android,所以我们默认只有这两个platform)。

  1. 我们需要通过一个临时文件来保存当前的env的状态,否则可能在已添加android平台的情况下,再添加ios会导致环境参数不一致的问题(如先添加devandroid,再添加ios时,由于错误添加了uat的版本)
  2. 有些Cordova plugin,如Google Maps,替换对应的key时,不是只修改package.json|config.xml下的参数就能正确实现切换的,只有remove plugin后重新添加plugin时才能正确切换
  3. etc...

实现自动修改config.xml

我们需要修改config.xml中的idversionname三个参数,所以,创建文件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

做完上面那一步,已经可以实现了。但是,前文中提到的case,还没有解决,所以我们还需要解决它
创建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>

测试

执行

 ENV_MODE=dev ionic cordova build android

查看config.xml,成功

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

依据环境切换第三方服务的Key[Google Maps为例]

在前面创建的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}时调用
我们创建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实现

  1. 创建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. 创建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>
    

测试

执行(测试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

上一篇 下一篇

猜你喜欢

热点阅读