ionic 3 開發(fā)環(huán)境切換
解決的問題
- 項目開發(fā)時,瀏覽器調(diào)試存在的
CORS(跨域)問題 - 編譯
ios或android時,(半)自動化切換環(huán)境的問題 - 根據(jù)不同的環(huán)境,使用不同的
key(如果Google Maps key) -
Android打包簽名的key
開始
項目假設有3中環(huán)境:
-
dev,開發(fā)者本機開發(fā)環(huán)境 -
uat,用戶測試環(huán)境 -
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)境切換,需要的步驟如下:
- 編寫
js腳本,在執(zhí)行編譯命令時,讀取、整合environment.*.ts配置,生成整合后的臨時文件environment.tmp. - 將
ionic的編譯配置指向臨時文件environment.tmp. - 編譯
在編寫腳本之前,需要將嵌入腳本:
修改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 CLI的Hooks
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.*.ts的cordova中已經(jīng)定義的屬性,我們在編譯前讀取屬性,再替換就能實現(xiàn)我們要的效果
思路
這里可能有幾個case需要考慮(鑒于我們現(xiàn)在基本只要照顧ios&android,所以我們默認只有這兩個platform)。
- 我們需要通過一個臨時文件來保存當前的
env的狀態(tài),否則可能在已添加android平臺的情況下,再添加ios會導致環(huán)境參數(shù)不一致的問題(如先添加dev的android,再添加ios時,由于錯誤添加了uat的版本) - 有些
Cordova plugin,如Google Maps,替換對應的key時,不是只修改package.json|config.xml下的參數(shù)就能正確實現(xiàn)切換的,只有remove plugin后重新添加plugin時才能正確切換 - 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)
-
創(chuàng)建
debug-keys.jks及release-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 -
編寫對應的
*.properties文件
如debug-signing.propertie:storeFile=debug-keys.jks storePassword=debugdebug keyAlias=debug keyPassword=debugdebug -
創(chuàng)建
scripts/hooks/move_android_keys.js
move_android_keys.jsvar 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)); }); } -
在
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