Ionic 3 升 Ionic 4 这件事,最容易让人低估的地方在于它不是一次版本升级,而是一次「换底座」。Ionic 3 是把 Angular 项目包了一层,你写的是 Ionic 的页面、Ionic 的导航、Ionic 的懒加载;Ionic 4 把这层壳拆了,底下就是一个标准 Angular 项目,组件改成了 Web Components,路由交还给 Angular Router。
结果就是,你以为改改包名就能跑,实际上路由、生命周期、样式、控制器调用方式全都要动。这篇是我当年对着两个版本逐项比对留下的记录,按九个维度列清楚哪里变了、旧写法对应的新写法长什么样。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
Ionic 4项目结构变成了什么样,ionic start的参数要怎么带才不会又建出一个 v3 项目provider改叫service、ionic-angular改叫@ionic/angular这些命名和依赖的变动- 样式为什么不能再写
page-xxx {},全局样式该放哪 - 从
Push/Pop导航切到Angular Router要改哪些地方,两套导航能不能混用 - 页面生命周期哪些保留、哪些被
Angular的钩子接管了 - 懒加载从
@IonicPage换成loadChildren的完整对照 Loading/Toast/Alert从同步创建改成Promise之后,代码要怎么改- 这套迁移在今天还值不值得做
# 一、项目结构的差异
先说结论,Ionic 4 项目就是一个 Angular 项目,没有别的了。
不
ionic了,以后都angular了。命名方式都用angular的了;provider也改成angular的叫法了,以后请叫service
这句是当年记的原话,糙但准。Ionic 3 里 ionic g provider MyData 生成的那个东西,在 Ionic 4 里就是 Angular 的 @Injectable() 服务,连生成命令都建议直接用 ng generate service。心里先完成这个切换,后面很多变更就顺理成章了。
创建项目的命令签名是这样:
ionic start <name> <template> [options]
ionic start myApp
ionic start myApp blank
ionic start myApp tabs --cordova
ionic start myApp tabs --capacitor
ionic start myApp tabs --type=angular
ionic start myApp blank --type=ionic1
这里有个坑要注意。原生能力那一层除了 Cordova 之外多了 Capacitor 的选项,这是 Ionic 官方自己做的替代方案;而想创建 Angular 版本的 Ionic 4 项目,必须带 --type=angular 参数,不带参数创建出来的是 ionic3 项目:
ionic start myApp tabs --type=angular
这条当时坑了不少人,命令跑完没报错,项目也能起,结果照着 v4 文档写代码处处对不上,回头一看目录结构还是 v3 的。
当然也可以用
angular-cli创建普通Angular项目,然后npm添加@ionic/core模块,创建完成后到目录结构如下图所示,它不再像ionic3那样封装了angular项目,而是直接就是一个angular项目,而且默认懒加载
下面这张是新项目的顶层目录,注意 src 平级多出来的 angular.json、e2e、karma.conf.js 这些标准 Angular CLI 产物。

再往里看 src/app,每个页面自带一个 xxx-routing.module.ts,这就是「默认懒加载」的来源。

看到这两张图就该明白了,往后你查文档的顺序要反过来:先查 Angular 怎么做,再看 Ionic 在这基础上加了什么。
# 二、配置和代理
v4同angular配置保持一致,可以在angular.json中进行一系列配置,代理则通过proxy.config.json配置
Ionic 3 时代改构建配置是件麻烦事,得去动 @ionic/app-scripts 的 config 目录,能改的口子还很有限。v4 之后这些统统落到 angular.json 里,assets、styles、scripts、budgets、多环境的 fileReplacements 全都是 Angular CLI 那一套。
本地联调跨域走 proxy.config.json,配好之后启动命令要带上,比如 ionic serve --proxy-config proxy.config.json,或者干脆写进 angular.json 的 serve 配置里,省得每次手打。这块完全是 Angular 的能力,Ionic 只是透传。
# 三、依赖变更
ionic-angular引入变为了@ionic/angularrxjs变化主要是由于rxjs5.5引入了Pipeable Operator,参考这里rxjs
第一条是全局替换就能解决的机械劳动,但要小心从 ionic-angular 里导出的那些东西不是一一对应搬过去的,ViewController 这类在 v4 里干脆没了(第八节会说)。
第二条影响更大一些。RxJS 5.5 引入了 Pipeable Operator 之后,链式的 .map().filter() 要改成 pipe(map(), filter()),导入路径也从 rxjs/add/operator/map 换成了 rxjs/operators。项目里 HTTP 请求处理写得多的话,这一块的改动量比换包名大得多。
// 旧写法
import 'rxjs/add/operator/map';
this.http.get(url).map(res => res.json()).subscribe(...)
// 新写法
import { map } from 'rxjs/operators';
this.http.get(url).pipe(map(res => res)).subscribe(...)
顺带提一句,Angular 的 HttpClient 已经自动帮你解析 JSON 了,那句 res.json() 在新写法里是多余的,留着反而会报错。这个我排查过一次,一直以为是 pipe 用错了。
# 四、CSS 的变更
- 和
angular保持一致,采用style或styleUrls方式引入,不再使用page-**{}方式 - 全局样式:可以将
v3中全局样式放置到global.scss中,也可以创建一个新的scss引入到angular.json中
那为什么 v3 要写 page-home { ... } 这么一层包裹呢?因为当年没有样式隔离,全靠这个类名手工圈定作用域,谁忘了包一层,样式就会漏到别的页面去。v4 用的是 Angular 的组件样式封装,写在 home.page.scss 里的规则默认只作用于这个组件,那层包裹就没有存在的必要了,留着反而会因为多一层选择器导致规则失效。
全局样式统一放 global.scss:

迁移的时候一个偷懒但有效的做法,是先把 v3 那些 page-xxx 包裹整体挪进 global.scss,保证页面先跑起来不变形,再一个页面一个页面往组件样式里拆。一次性全拆容易改到崩,改完还找不出是哪条规则丢了。
# 五、路由差异
angular路由官方文档 https://angular.cn/guide/router
这是整个迁移里改动最大的一块。
也许
Ionic 4中最显着的变化,以及需要对现有应用程序进行最大改变的变化,是转向Angular风格的路由。Ionic过去使用的典型Push/Pop风格导航仍然可用,您甚至可以直接通过Ionic的Web组件使用这种导航方式,但推荐的方法是使用Angular Router
观察目录结构,很容易发现这是一个 angular 项目,是因为它有一个 routing 模块:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', loadChildren: './tabs/tabs.module#TabsPageModule' }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
loadChildren 这里写的是字符串形式 '路径#模块名',这是当年的写法,我原样保留。Angular 8 之后官方改推动态 import 的形式(loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)),原因是字符串形式没法被 TypeScript 类型检查和 IDE 跳转,写错模块名要跑起来才知道。老项目升 Angular 大版本时这块是必改项。
而对应的路由组件是
ion-router-outlet,是对Angular的router-outlet扩展,以兼容旧的导航方式,打开tabs.page.html可看到下面内容
<ion-tabs>
<ion-tab label="Home" icon="home" href="/tabs/(home:home)">
<ion-router-outlet name="home"></ion-router-outlet>
</ion-tab>
<ion-tab label="About" icon="information-circle" href="/tabs/(about:about)">
<ion-router-outlet name="about"></ion-router-outlet>
</ion-tab>
<ion-tab label="Contact" icon="contacts" href="/tabs/(contact:contact)">
<ion-router-outlet name="contact"></ion-router-outlet>
</ion-tab>
</ion-tabs>
这段是 Ionic 4 beta 阶段的 tabs 写法,用的是 Angular 的辅助路由(那对括号 (home:home) 就是 named outlet 语法)。正式版之后 tabs 的结构调整成了 ion-tabs 里放 ion-tab-bar 加 ion-tab-button,标签的路由改由 tabs-routing.module.ts 里的 children 配置,不再用辅助路由。你手上的模板长什么样以你安装的版本和官方文档为准,我把当年这段留在这,是因为不少 2018 到 2019 年间起的项目里就是这个样子,看到了不至于以为写错了。
ion-router-outlet 值得单独说一句。它是 Angular router-outlet 的扩展版,多做的事情是维护一个页面栈并处理转场动画。所以在 Ionic 应用里别用原生的 router-outlet 替换它,换了之后路由能跳,但页面切换会变成生硬的整屏替换,返回手势也没了。