Git Product home page Git Product logo

blog's Introduction

Top Langs jadertao's github stats

blog's People

Contributors

jadertao avatar

Stargazers

 avatar

Watchers

 avatar

blog's Issues

<译> 英雄项目的 NativeScript 迁移

date: 2019-01-07

译者注: 本文是我为 Stanimira Vlaeva from NativeScriptng china 2018 workshop 翻译的讲稿。原文翻译

英雄项目是 Angular 官方的入门教程。

英雄项目的 NativeScript 迁移之旅

欢迎来到英雄项目 NativeScript 化迁移之旅的教程!你可以学到如何将你的 Angular web 项目扩展成能运行在 Android 和 iOS 上的应用。

准备

NativeScript 配置

你需要 NativeScript CLI 和一台 Android 或 iOS 物理设备来完成这个教程。

  1. 运行下面的命令来安装 NativeScript CLI:
npm install -g nativescript@latest
  1. 在等待 npm install 的过程中,你可以拿起手机安装两个配合运行的 app:
app Android iOS
NativeScript Preview Google Play App Store
NativeScript Playground Google Play App Store

Angular 配置

Angular CLI 提供了一些方便迁移和管理 Angular 项目的命令。 NativeScript 用自身的 schematics 集合扩展了 Angular CLI 的内置命令。通过执行下面的命令来安装 Angular CLI 和 NativeScript 扩展。

npm install -g @angular/cli @nativescript/schematics

应用

今天你将会对 Angular 官方文档教程 中的英雄之旅项目进行迁移。

首先克隆这个项目的 web 完全版。

git clone https://github.com/sebawita/tour-of-heroes/
cd tour-of-heroes
npm install

将项目转换成代可以进行代码共享的结构

Angular CLI 提供的 ng add 命令让你可以为 Angular 项目添加不同的功能,比如 Angular material, ngRx, Firebase, Angular Apollo,当然也包括 NativeScript。
你可以使用 ng add 命令将 web 项目转换成供 Web 和 NativeScript 共享代码的项目结构。在项目中运行下面的命令即可:

ng add @nativescript/schematics

验证迁移

确保转换顺利最简单的方式就是运行一下 web 和移动项目,亲眼看一下它们是否正常工作。

  1. Web 端

运行下面的命令来启动 web 端应用:

ng serve --open

当构建完成时,你的浏览器会自动打开一个新的标签页,页面中运行着之前的英雄之旅应用。

  1. 移动端

运行下面的命令来启动移动端应用:

tns preview --bundle

此命令会在命令行终端输出一个二维码。打开你手机中的 NativeScript Playground 应用,用 Scan QR code 功能扫描命令行中的二维码。

扫描之后,NativeScript Preview 应用即会启动。一开始应用可能会白屏。先不要失望哦,等几秒(WiFi 信号差时可能慢至一分钟)你就会看到一个大按钮,告诉你 AUTO-GENERATED WORKS!。这样你就得到了你第一个 NativeScript Angular。爽不爽?

提示:如果无法扫描二维码,可以试着更换命令行的颜色。

进行第一次修改

你不必在每次更新应用时都去扫描二维码。NativeScript CLI 会自动推送所有已保存的变更。

让我们来试一下。

  1. 打开 src/app/auto-generated/auto-generated.component.tns.html
  2. text 属性改为 text="Hello World"
  3. 保存文件。

现在你应该会看到 app 中的大按钮写着 Hello World 💭🌍

导航

在迁移后的项目中你会发现联众不同的路由 NgModule

  • app-routing.module.tns.ts - 一个 NativeScript 端路由 NgModule。

代码共享的项目使用命名约定来区分 web 端和移动端文件。NativeScript 特定的文件后缀为 .tns。 在 NativeScript 文档的 代码分割 章节你可以了解更多。

打开文件 app-routing.module.tns.ts,你会注意到 NativeScript 引入了 NativeScriptRouterModule。这个模块封装了 Angular 提供的 RouterModule。NativeScript 扩展了 Angular 的默认路由,添加了一些移动设备的功能,比如屏幕之间的过渡动画。

你还会看到一个 routes 数组。它包含了一个指向 HomeComponent 的路由。在后面的教程中,我们会将 routes 移至一个普通的文件,以在 web 端和移动端的 NgModule **享。

迁移项目内容

ng add @nativescript/schematics 这个命令将项目转换为代码共享的结构。然而它并没有转换应用的逻辑。关于自动生成的部分先讲到这里,是时候自己亲自动手写一写代码了!

接下来的几步:

  • 迁移 AppModule
  • 迁移其它的 modules
  • (可选) 统一 navigation

迁移 AppModule

在这部分中,你将会编辑:

  • app.module.ts - web 端的根模块;
  • app.module.tns.ts - 移动端的根模块.

根模块 NgModule(通常叫做 AppModule)的迁移包括寻找它引入的 NgModule 的等价模块以及迁移它声明的组件。

迁移 AppModule 的引入模块

让我们看一下 web 端 NgModule 的元信息中引入了哪些模块。

app.module.ts

imports: [
  BrowserModule,
  FormsModule,
  AppRoutingModule,
  HttpClientModule,

  HttpClientInMemoryWebApiModule.forRoot(
    InMemoryDataService, {dataEncapsulation: false}
  )
],

imports 部分的迁移工作指的是将 web 端文件 (app.module.ts) 改为移动端文件(app.module.tns.ts)。对每个引入的模块你都需要检查:

  • 如果这是 平台无关的引入 - 意味着它在 web 和 {N}(注:{N} 指 NativeScript 环境)两种环境中均可工作 - 把它添加至 app.module.tns.ts 即可;
  • 如果是平台相关的引入 - 我们需要找到 {N} 等价模块 并把等价模块添加至 app.module.tns.ts

BrowserModule

BrowserModuleweb 特定 的模块。
它在 {N} 中的等价模块是 NativeScriptModule。 你的 app.module.tns.ts 已经引入了NativeScriptModule,这里不需再做额外工作。

FormsModule

FormsModule 也是 web 特定 的模块。
它在 {N} 中的等价模块是 NativeScriptFormsModule。你的 app.module.tns.ts 中有一条被注释掉的对它的引入,取消注释即可把它引入至NgModule imports

AppRoutingModule

这个阶段我们有两个版本的 AppRoutingModule

  • Web - app-routing.module.ts - 包含 routes 来对你原来的 web 组件进行导航,
  • {N} - app-routing.module.tns.ts - 包含 routes 来导航至 HomeComponent 样例组件.

稍后我们将详细探讨导航,现在我们先讨论如何将两种配置集合在一起。
不过,在迁移工作的这个阶段,还是将它们分开比较好。

你的 app.module.tns.ts 早已引入了 {N} AppRoutingModule,所以不需再做额外工作。

HttpClientModule

HttpClientModule 实际上不是 web 特定的。你也可以在移动应用中使用。但是,NativeScript 提供了相似的模块 - NativeScriptHttpClientModule。它扩展了默认的 Angular HTTP client,对移动端更加友好。我们推荐你优先使用 {N} module。

你的 app.module.tns.ts 已经有一条被注释了的对NativeScriptHttpClientModule的引入,取消注释即可把它引入至NgModule imports

HttpClientInMemoryWebApiModuleInMemoryDataService

HttpClientInMemoryWebApiModuleInMemoryDataService 是平台无关的模块。把他们的引入从 app.module.ts 复制到 app.module.tns.ts 后我们就能顺利进行下去了。

Module 引入总结

这是你 NativeScript 的 imports 应该的样子:

app.module.tns.ts

import { NativeScriptModule } from 'nativescript-angular/nativescript.module';
import { NativeScriptFormsModule } from 'nativescript-angular/forms';
import { NativeScriptHttpClientModule } from 'nativescript-angular/http-client';

import { AppRoutingModule } from './app-routing.module.tns';

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
...

@NgModule({
  ...
  imports: [
    NativeScriptModule,
    NativeScriptFormsModule,
    AppRoutingModule,
    NativeScriptHttpClientModule,

    HttpClientInMemoryWebApiModule.forRoot(
      InMemoryDataService, {dataEncapsulation: false}
    ),
  ],
  ...
})
export class AppModule { }

这是一张所有 NgModule imports 表:

Name Same Module {N} Name import from
BrowserModule No NativeScriptModule 'nativescript-angular/nativescript.module'
FormsModule No NativeScriptFormsModule 'nativescript-angular/forms'
AppRoutingModule No AppRoutingModule './app-routing.module.tns'
HttpClientModule No NativeScriptHttpClientModule 'nativescript-angular/http-client'
HttpClientInMemoryWebApiModule Yes --- ---
InMemoryDataService Yes --- ---

迁移 AppModule 组件

下一步的任务是将 AppModule Components 迁移成代码共享的结构。

通常,组件的迁移由三个步骤组成:

  1. 创建一个 {N} 版本的模板文件(例如 name.component.tns.html
    • 为它加上 {N} UI Code
  2. (可选)创建一个 {N} 版本的样式文件(例如:name.component.tns.css
    • 为它加上 {N} UI Code
  3. {N} AppModule (app.module.tns.ts)的 Declarations 中加入该组件
    (可选)将该组件添加至 {N} 导航。

这步任务可以在 migrate-component schematic 的帮助下进行。它会自动完成前三步。通过下面的命令执行它:

ng g migrate-component --name=component-name

迁移 HeroesComponent

首先进行 HeroesComponent 的组件迁移:

ng g migrate-component --name=heroes

这个命令通过如下方式更改你的项目:

  • 使用 heroes.component.html 中注释掉的 html 生成 heroes.component.tns.html
  • 生成 heroes.component.tns.css 空文件;
  • HeroesComponent 添加至 app.module.tns.tsdeclarations 数组中。
src
└── app
    ├── heroes
    |   ├── heroes.component.html
    |   ├── heroes.component.tns.html <= create
    |   └── heroes.component.ts
    |   └── heroes.component.tns.css  <= create
    |   └── heroes.component.css
    ├── app.module.tns.ts             <= update
    └── app.module.ts

将组件加到移动端的导航中

你需要更新 NativeScript 的导航 routes,让它可以在应用启动时导航至新创建的 HeroesComponent

app-routing.module.tns.ts

import { HeroesComponent } from './heroes/heroes.component';

export const routes: Routes = [
  {path: '', redirectTo:'/heroes', pathMatch:'full'},
  {path: 'heroes', component: HeroesComponent},
];

你会看到屏幕中有一条简单的消息: Heroes Component works

如果你没有看到变化,试着重新构建应用。停止命令行的当前进程并再次执行预览命令:

tns preview --bundle

更新模板

是时候创建你第一个移动端页面了!

NativeScript UI 组件是一个广泛的话题,今天的时间比较宝贵,我们不会深入探讨它。如果你想了解更多,可以看看这些资料:

好了,让我们开始吧。html 模板看起来像这样:

heroes.component.html

<h2>My Heroes</h2>

<div>
  <label>Hero name:
    <input #heroName />
  </label>
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
    <button class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
  </li>
</ul>

一开始,是:

heroes.component.html

<h2>My Heroes</h2>

这显然是页面的标题。你可以用 ActionBar 来替换。

heroes.component.tns.html

<ActionBar title="My Heroes" class="action-bar">
</ActionBar>

接下来是一个包含着一个 label,一个 input 和一个 button 的 div 容器。

heroes.component.html

<div>
  <label>Hero name:
    <input #heroName />
  </label>
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </button>
</div>

你需要进行如下替换:

  • <div> 替换为 <GridLayout>,排成三列;
  • <label> 标签替换为 <Label> 标签(首字母大写),将**Hero name:**的值赋给标签的 [text] 属性;
  • <input> 替换为 <TextField>
  • <button> 替换为 <Button> - 在这里你传入 heroName.text(而不是 heroName.value);
  • (click) 事件属性替换为 (tap) 事件属性。

像这样:

heroes.component.tns.html

<GridLayout rows="auto" columns="auto, *, auto">
  <Label text="Hero name:" class="h2 m-l-5"></Label>
  <TextField col="1" hint="enter hero name..." #heroName class="m-x-5"></TextField>
  <Button col="2" text="add" (tap)="add(heroName.text); heroName.text=''" class="btn btn-primary"></Button>
</GridLayout>

下面是英雄列表:

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
    <button class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
  </li>
</ul>

你需要进行如下替换:

  • 将无序列表 <ul> 替换为 <ListView>
  • 将每一个 <li> 替换为 <ng-template>
  • 将导航链接 <a> 替换为 GridLayout。同时将 routerLink 指令换为 nsRouterLink
  • <span> 替换为<Label>
  • <button> 替换为 <Button>

像这样:

heroes.component.tns.html

<ListView [items]="heroes" class="list-group" height="100%">
  <ng-template let-hero="item">
    <GridLayout columns="auto, *, auto" nsRouterLink="/detail/{{hero.id}}">
      <Label col="0" [text]="hero.id" class="list-group-item"></Label>
      <Label col="1" [text]="hero.name" class="list-group-item"></Label>
      <Button col="2" text="X" (tap)="delete(hero)" class="btn btn-primary"></Button>
    </GridLayout>
  </ng-template>
</ListView>

最后,你需要用 StackLayoutGridLayoutListView 包起来。

请注意,包括 ActionBar 在内的 NativeScript 模板最多仅能包含一个组件。这就是你需要一个布局容器把其他组件囊括起来的原因。

整个模板会像这样:

heroes.component.tns.html

<ActionBar title="My Heroes" class="action-bar">
</ActionBar>

<StackLayout>
  <GridLayout rows="auto" columns="auto, *, auto">
    <Label text="Hero name:" class="h2 m-l-5"></Label>
    <TextField col="1" hint="enter hero name..." #heroName class="m-x-5"></TextField>
    <Button col="2" text="add" (tap)="add(heroName.text); heroName.text=''" class="btn btn-primary"></Button>
  </GridLayout>

  <ListView [items]="heroes" class="list-group" height="100%">
    <ng-template let-hero="item">
      <GridLayout columns="auto, *, auto" nsRouterLink="/detail/{{hero.id}}">
        <Label col="0" [text]="hero.id" class="list-group-item"></Label>
        <Label col="1" [text]="hero.name" class="list-group-item"></Label>
        <Button col="2" text="X" (tap)="delete(hero)" class="btn btn-primary"></Button>
      </GridLayout>
    </ng-template>
  </ListView>
</StackLayout>

更新样式

你可以用 CSS 来为 NativeScript 编写样式。为了让上面的组件更有吸引力,把下面的样式复制到 heroes.component.tns.css

.container {
  background-color: #EFEFF4;
}

.search-container {
  padding: 15;
}

.search-container TextField {
  color: #757575;
  padding-left: 10;
  font-size: 20;
}

.search-container Button {
  background-color: #30CE91;
  color: white;
  font-size: 18;
  padding: 10 30;
  border-radius: 20;
}

ListView {
  background-color: transparent;
  separator-color: transparent;
}

.hero-container {
  background-color: white;
  border-radius: 10;
  margin: 10 15;
  padding: 15;

}
.circle {
  background-color: #C6D7FE;
  color: #2A48CD;
}

.hero-label {
  font-weight: bold;
  font-size: 18;
  margin-left: 10;
}

.delete-button {
  font-size: 24;
  font-weight: normal;
}

迁移 HeroDetailComponent

下一步你需要迁移 HeroDetailComponent

步骤与前面相同.

  1. 运行迁移命令.

    ng g migrate-component --name=hero-detail
  2. 在移动端导航配置中注册新的组件。

    app-routing.module.tns.ts

    import { HeroDetailComponent } from './hero-detail/hero-detail.component';
    
    export const routes: Routes = [
      {path: '', redirectTo:'/heroes', pathMatch:'full'},
      {path: 'heroes', component: HeroesComponent},
      {path: 'detail/:id', component: HeroDetailComponent},
    ];
  3. 更新 NativeScript 模板。

hero-detail.component.tns.html

<ActionBar title="{{hero?.name | uppercase}} Details" icon=""class="action-bar">
</ActionBar>

<StackLayout *ngIf="hero">
  <Label text="ID: {{hero.id}}" class="h1 text-center"></Label>
  <TextField [(ngModel)]="hero.name" hint="name" class="input input-border"></TextField>

  <Button text="Go Back" (tap)="goBack()" class="btn btn-primary"></Button>
  <Button text="Save" (tap)="save()" class="btn btn-primary"></Button>
</StackLayout>
  1. 更新样式。

hero-detail.component.tns.css

.container {
  background-color: #EDECF2;
}

.main-name {
  background-color: #2A48CD;
  font-size: 36;
  font-weight: bold;
  text-align: center;
  color: white;
  padding: 0 0 15 0;
}

.grid {
  margin: 30;
}

.circle {
  background-color: #C6D7FE;
  color: #2A48CD;
}

.name {
  font-size: 22;
  font-weight: bold;
  margin-left: 20;
}

.id-label {
  text-align: center;
}

.name-label {
  margin-left: 20;
}

.id-label, .name-label {
  margin-top: 10;
  color: #ABABAB;
}

.btn {
  background-color: #2A48CD;
  color: white;
  font-weight: bold;
  font-size: 18;
  padding: 15 0;
  border-radius: 25;
}
  1. 试一下新页面

运行 app 并导航到一个英雄页面再返回。重复几次以确定程序正常运行。

迁移 DashboardComponent 和 HeroSearchComponent

DashboardComponent 使用了 HeroSearchComponent,这意味着你需要同时迁移这两个组件。

  1. 为这两个组件运行迁移命令:
  ng g migrate-component --name=dashboard
  ng g migrate-component --name=hero-search
  1. 更新导航

在路由中为 DashboardComponent 添加一个新的路径。
但是,你不需要为 HeroSearchComponent 再添加路径,因为可以通过 选择器直接使用它。

app-routing.module.tns.ts

import { DashboardComponent } from './dashboard/dashboard.component';

export const routes: Routes = [
  {path: '', redirectTo:'/heroes', pathMatch:'full'},
  {path: 'heroes', component: HeroesComponent},
  {path: 'detail/:id', component: HeroDetailComponent},
  {path: 'dashboard', component: DashboardComponent},
];
  1. 更新 the NativeScript 模板

dashboard.component.tns.html

<ActionBar title="Top Heroes" icon=""class="action-bar">
</ActionBar>

<GridLayout rows="*, *">
  <ListView row="0" [items]="heroes" class="list-group">
    <ng-template let-hero="item">
      <Button [text]="hero.name" nsRouterLink="/detail/{{hero.id}}" class="btn btn-primary"></Button>
    </ng-template>
  </ListView>
  <StackLayout row="1">
    <app-hero-search row="1"></app-hero-search>
  </StackLayout>
</GridLayout>

hero-search.component.tns.css

<StackLayout>
  <Label text="Hero Search" class="h1 text-center"></Label>

  <TextField #heroName
    hint="enter hero-name"
    autocorrect="false"
    class="m-x-5"
    (loaded)="heroName.text =''"
    (textChange)="search($event.value)">
  </TextField>
  <ListView [items]="heroes$ | async" class="list-group">
    <ng-template let-hero="item">
      <Label [text]="hero.name" nsRouterLink="/detail/{{hero.id}}" class="list-group-item"></Label>
    </ng-template>
  </ListView>
</StackLayout>

有趣的是,<app-hero-search> 选择器在 web 端和移动端都可使用。因为 HeroSearchComponent 是平台无关的。

  1. 更新 NativeScript 样式:

dashboard.component.tns.css

.header {
    font-size: 42;
    font-weight: bold;
    color: white;
    text-align: center;
    margin: 0 0 20 0;
}

FlexboxLayout {
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    align-content: flex-start;
    margin-top: 30;
}
.hero-container {
    width: 49%;
    border-width: 1;
    border-color: #395ad9;
}

.name {
    color: white;
    font-size: 20;
    text-align: center;
    margin: 10 0 20 0;
}

.circle {
    margin-top: 20;
}

hero-search.component.tns.css

.search-result li {
  border-bottom: 1px solid gray;
  border-left: 1px solid gray;
  border-right: 1px solid gray;
  width:195px;
  height: 16px;
  padding: 5px;
  background-color: white;
  cursor: pointer;
  list-style-type: none;
}

.search-result li:hover {
  background-color: #607D8B;
}

.search-result li a {
  color: #888;
  display: block;
  text-decoration: none;
}

.search-result li a:hover {
  color: white;
}
.search-result li a:active {
  color: white;
}
#search-box {
  width: 200px;
  height: 20px;
}


ul.search-result {
  margin-top: 0;
  padding-left: 0;
}
  1. 试一下组件。

此时,你的移动端应用还无法导航至 Dashboard。 导航到它最简单的方法就是修改默认 redirectTo 路径为'/dashboard'。像这样:

app-routing.module.tns.ts

export const routes: Routes = [
  {path: '', redirectTo:'/dashboard', pathMatch:'full'},
  {path: 'heroes', component: HeroesComponent},
  {path: 'detail/:id', component: HeroDetailComponent},
  {path: 'dashboard', component: DashboardComponent},
];

但一定要记得测试完改回去。

SideDrawer 导航

你现在还缺一个好方法实现在英雄页和 Dashboard 页之间导航。通常可以用 TabViewSideDrawer

在本教程中,我们选择有一系列按钮的 SideDrawer 导航方式。

添加 SideDrawer 插件

运行下面的命令以安装 NativeScript SideDrawer

tns plugin add nativescript-ui-sidedrawer

更新 {N} AppModule

然后你需要在 NativeScript AppModule 中引入 NativeScriptUISideDrawerModule

app.module.tns.ts

import { NativeScriptUISideDrawerModule } from 'nativescript-ui-sidedrawer/angular';

@NgModule({
  imports: [
    ...
    NativeScriptUISideDrawerModule,
  ]
  ...
})
export class AppModule { }

添加 SideDrawer

在项目中添加 SideDrawer 非常容易。 就像 web 项目 app.component.html 中导航定义的那样,你可以将 SideDrawer 添加到 app.compoment.tns.html

app.component.tns.html

<RadSideDrawer>
  <GridLayout tkDrawerContent rows="*" class="sidedrawer sidedrawer-left">
    <StackLayout class="sidedrawer-content">
      <Button text="heroes" nsRouterLink="/heroes" (tap)="closeDrawer()" clearHistory="true" class="btn btn-primary"></Button>
      <Button text="dashboard" nsRouterLink="/dashboard" (tap)="closeDrawer()" clearHistory="true" class="btn btn-primary"></Button>
    </StackLayout>
  </GridLayout>

  <page-router-outlet tkMainContent class="page page-content"></page-router-outlet>
</RadSideDrawer>

注意,<GridLayout> 包含着 SideDrawer 的内容(你需要在这添加所有的导航链接)。但是页面的内容会在 <page-router-outlet> 处加载。

此外, 每个按钮都同时使用了 nsRouterLink='/path'clearHistory="true"。这是为了防止 iOS 在 ActionBar 上添加后退按钮,并防止 Android 在用户使用设备的返回功能时回退。

最后,每个按钮调用 closeDrawer() 函数,为了在应用导航后 SideDrawer 自己进行优雅的关闭,以提供良好的用户体院。

<Button
	text="heroes"
	nsRouterLink="/heroes"
	clearHistory="true"
	(tap)="closeDrawer()"
	class="btn btn-primary">
</Button>

你需要在 app.component.tns.ts 中添加 closeDrawer() 函数,像这样:

import { Component } from '@angular/core';
import * as app from 'tns-core-modules/application';
import { RadSideDrawer } from 'nativescript-ui-sidedrawer';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
})

export class AppComponent {
  closeDrawer() {
    const sideDrawer = <RadSideDrawer>app.getRootView();
    sideDrawer.closeDrawer();
  }
}

测试 SideDrawer

现在你可以在屏幕左侧滑动手指,SideDrawer 会出现。当你点击任何一个按钮时,SideDrawer 会关闭并导航至对应页面。

共享导航

分割路由

在这个阶段路由会被分割为两个不同的配置,分别应用在 web 端和移动端。

这是你启动一个 NativeScript 项目时,它会先展示 HomeComponent 的原因。

在你将你的 web 组件迁移成代码共享结构时,路由分割通常是有用的。一旦迁移完成,你可以为双端选择一份单一的配置。

并且,如果你希望你的 web app 相比 mobile app 有不同的页面,你也可以使用两套不同的路由。比如是,你的 web app 有一个管理页面,但 mobile app 并不需要。这种情况下使用两套导航配置比较合理。

这个阶段在你迁移每个页面组件时,你需要在 app-routing.module.tns.ts 的路由数组里添加对应的导航路径。

共享路由

这个项目你可以简单地使用同一套 routes 配置,只需要将 routes 配置移到一个共享的文件。

步骤 1 - 创建一个共享的路由文件。

创建一个名为 app.routes.ts 的新文件 并将 app-routing.module.ts 中的 routes 数组复制过来。

确保导出了路由,确保引入了所有组件。

app.routes.ts

import { Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

export const routes: Routes = [
  {path: '', redirectTo:'/heroes', pathMatch:'full'},
  {path: 'heroes', component: HeroesComponent},
  {path: 'dashboard', component: DashboardComponent},
  {path: 'detail/:id', component: HeroDetailComponent},
];

Step 2 - Update both app-routing files with the shared routes property

步骤 2 - 使用共享的 routes 属性更新两个 app-routing 文件

用从 './app.routes' 引入的 routes 更新两个 app-routing 文件中 routes 的值。然后清理掉未使用的引入。

app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { routes } from './app.routes';

@NgModule({
  imports: [
    RouterModule.forRoot(routes)
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }

app-routing.module.tns.ts

import { NgModule } from '@angular/core';
import { NativeScriptRouterModule } from 'nativescript-angular/router';
import { routes } from './app.routes';

@NgModule({
  imports: [
    NativeScriptRouterModule.forRoot(routes)
  ],
  exports: [
    NativeScriptRouterModule
  ]
})
export class AppRoutingModule { }

现在你有了一个双端共享的导航配置。

圆满完成

Congratulations! 你成功地将英雄之旅 app 转化为跨平台的项目,它可以运行在 web,Android 和 iOS 上。
如果你想阅读更多关于与 NativeScript 共享代码和迁移 web 应用的资料,可以看一下官方文档:

Migrating a Web Project

Credits: This tutorial is created by Sebastian Witalec and partly edited by Stanimira Vlaeva.

gulp glob技术内幕

最近为团队做了一个小程序增量构建工具用到了gulp,核心的『直接构建』和『watch 构建』分别依赖了gulp#srcgulp#watch。复用glop是遇到了相同glob在两个 api 中表现不一致的问题,翻了一下两个 api 的实现特此记录一下。
这里的watchgulp主包的 api,不是gulp-watch

gulp#src

import { src } from 'gulp'

由引入方式可知src是根文件直接导出的,观察一下:

// index.js
var vfs = require('vinyl-fs');
...
function Gulp() {
  ...
  // Bind the functions for destructuring
  ...
  this.src = this.src.bind(this);
  ...
}
...
Gulp.prototype.src = vfs.src;
var inst = new Gulp();
module.exports = inst;

咦,src这么重要的 api 竟然直接用了别的库的实现,再看看vinyl-fs

// vinyl-fs/lib/src.js
var gs = require('glob-stream');
var isNegatedGlob = require('is-negated-glob');
...

function src(glob, opt) {
  ...
  var streams = [
    gs(glob, opt),
    ...
  ];
  ...
}

module.exports = src;

fine,继续看glob-stream

...
function globStream(globs, opt) {
  ...
  var positives = [];
  var negatives = [];

  globs.forEach(sortGlobs);

  function sortGlobs(globString, index) {
    if (typeof globString !== 'string') {
      throw new Error('Invalid glob at index ' + index);
    }

    var glob = isNegatedGlob(globString);
    var globArray = glob.negated ? negatives : positives;

    globArray.push({
      index: index,
      glob: glob.pattern,
    });
  }
  ...

  function streamFromPositive(positive) {
    var negativeGlobs = negatives
      .filter(indexGreaterThan(positive.index))
      .map(toGlob)
      .concat(ignore);
    return new GlobStream(positive.glob, negativeGlobs, ourOpt);
  }
}

function indexGreaterThan(index) {
  return function(obj) {
    return obj.index > index;
  };
}

function toGlob(obj) {
  return obj.glob;
}

module.exports = globStream;

逻辑并不复杂,后续还有几道处理,在这里不再赘述,直接给出流程:

  1. glob分类成匹配型和排除型并记录每个glob在原始传入的glob数组中的索引。
  2. 对每个匹配型glob与索引在其之后的排除型glob配对并一起传给GlobStream
  3. GlobStream对上一部的参数简单处理(获取base路径,计算绝对路径)后,构建一套规则。
  4. glob类库实际负责根据规则遍历文件数并找出匹配的文件,每个文件路径的都会经由minimatch类库走一边glob规则校验判断是否被排除,然后与GlobStream通过事件的形式传递数据。
  5. 函数栈的退出,数据通过steam的形式最终回到gulp#src

gulp#watch

import { watch } from 'gulp'

由引入方式可知src是根文件直接导出的,观察一下:

var watch = require('glob-watcher');

function Gulp() {
  ...
  // Bind the functions for destructuring
  this.watch = this.watch.bind(this);
  ...
}

Gulp.prototype.watch = function(glob, opt, task) {
  ...
  return watch(glob, opt, fn);
};

var inst = new Gulp();
module.exports = inst;

咦,glob未经处理直接传给了glob-watcher,继续看:

...
var chokidar = require('chokidar');
var isNegatedGlob = require('is-negated-glob');
var anymatch = require('anymatch');
...
function watch(glob, options, cb) {
  ...
  // These use sparse arrays to keep track of the index in the
  // original globs array
  var positives = new Array(glob.length);
  var negatives = new Array(glob.length);

  // Reverse the glob here so we don't end up with a positive
  // and negative glob in position 0 after a reverse
  glob.reverse().forEach(sortGlobs);

  function sortGlobs(globString, index) {
    var result = isNegatedGlob(globString);
    if (result.negated) {
      negatives[index] = result.pattern;
    } else {
      positives[index] = result.pattern;
    }
  }

  function shouldBeIgnored(path) {
    var positiveMatch = anymatch(positives, path, true);
    var negativeMatch = anymatch(negatives, path, true);
    // If negativeMatch is -1, that means it was never negated
    if (negativeMatch === -1) {
      return false;
    }

    // If the negative is "less than" the positive, that means
    // it came later in the glob array before we reversed them
    return negativeMatch < positiveMatch;
  }

  var toWatch = positives.filter(exists);

  // We only do add our custom `ignored` if there are some negative globs
  // TODO: I'm not sure how to test this
  if (negatives.some(exists)) {
    opt.ignored = [].concat(opt.ignored, shouldBeIgnored);
  }
  var watcher = chokidar.watch(toWatch, opt);

  return watcher;
}

module.exports = watch;

看一下watch的实现,也有类似的实现和类似的机制,比如:

  • 都会 分类glob并记录在原始数组中的索引。
  • 都有事件机制
  • 都会对获取到的文件路径进行glob规则校验
  • 匹配型和排除型glob的索引(其实这是一种约定,排除型glob必须在匹配型之后)

不同之处也很明显:

src watch
文件处理库 glob chokidar
文件规则 (glob) 匹配 minimatch anymatch
匹配实现场所 文件处理库内 文件处理库外

文件处理库的不同是因为各自功能不同,匹配实现场所是对封装风格的不同选择。

文件规则匹配实现的不一致则是造成相同glob无法在gulp#srcgulp#watch之间复用的核心原因。

总结

  1. gulp#srcgulp#watchglob处理的核心逻辑实现不一致,这是 gulp 的败笔。gulp 团队也一直未认识到这一点:

实际上对匹配机制的同一也是可实现的。只需将gulp#watch依赖的gulp-watcher中的shouldBeIgnoredminimatch重写anymatch的逻辑即可。

  1. gulp 的核心逻辑是vinyl-fs提供的,gulp 只是简单地包装了下 api。

TypeScript 中继承的实现

date: 2018-12-19

TL;DR

末尾有总结。

这篇文章我会探究 TypeScript 中 extends 的实现机理。

在 JavaScript 中实现继承是一个比较古老的话题,也是垃圾文章的重灾区。
诚然现如今我们可以在 tsc 加持的 TypeScript 和 babel 加持的 ES Next 中简单地使用 class extends 关键字实现继承,但理解如何在 ES5 中如何使用 prototype[[Prototype]]] 实现继承更能增加自己对 JavaScript 较细粒度上的理解。

广泛使用的方案其正确性也毋庸置疑,因此本文将以 TypeScript class extends 转换成的 ES5 代码作为重点分析。

之所以不分析 babel 的方案只是因为笔者更喜欢 TypeScript。

背景

面对对象编程(Object-oriented programming)是一种有对象概念的编程范式,也是一种程序开发的抽象方针。

它是一种范式,体现在某个具体的语言上则有不同的风格,比如说 Java、C++、Python 中的基于类(class)的面向对象风格,以及 JavaScript、Lua、Self 中的基于原型(prototype)的面向对象风格。

这里多说一句,JavaScript 的设计主要受到了 Self 和 Scheme 的影响,Self 语言是在 SmallTalk 语言的基础上发展而来的,StrongTalk 是 SmallTalk 的高性能 VM,而 JavaScript 性能最好的引擎 V8 又是吸收了 HotSpot 和 StrongTalk 的精华,V8 的仓库里至今仍有 StrongTalk 的 license

原料

把简单的 TypeScript 继承语句用 tsc 编译成 ES5,参考这里

// TypeScript
class Foo {
    static value: number = 1
    public foo: number
    constructor() {
        this.foo = 1
    }
    add1(v: number): number {
        return 1 + v
    }
}
class Bar extends Foo {
    static subValue: number = 2
    constructor() {
        super()
        console.log(this.foo);
    }
}

// -------
// TSC 编译
// -------

// ES5
var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({__proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d;}
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Foo = /** @class */ (function () {
    function Foo() {
        this.foo = 1;
    }
    Foo.prototype.add1 = function (v) {
        return 1 + v;
    };
    Foo.value = 1;
    return Foo;
}());
var Bar = /** @class */ (function (_super) {
    __extends(Bar, _super);
    function Bar() {
        var _this = _super.call(this) || this;
        console.log(_this.foo);
        return _this;
    }
    Bar.subValue = 2;
    return Bar;
}(Foo));

好了,开始对代码的分析,step by step,statement bt statement,expression by expression。

源码分析

类的属性和方法分两部分,静态属性和方法以及成员属性和方法。静态属性和方法体现为类构造函数的属性及方法,成员属性和方法体现为类构造函数的 prototype 对象的属性及方法。

extends 的实现

顶层声明的 __extends 函数毫无疑问是 extends 的 polyfill 实现。

一、

(this && this.__extends) || (function () {...})()

这里是防止 __extends 函数的重复声明。|| 左侧判断当前上下文是否存在及当前上下文中是否有 __extends 函数。若皆满足条件,则直接返回 this.extends;若不满足,则重新声明该函数,|| 右边的函数即其函数体。

  • this 是谁?

在 JavaScript 中,除非特殊指定,this 的指向的 context object 由调用时的对象和上下文决定,表现与 dynamic scope 相似。(而闭包的建立是 lexical scope/static scope)

  • 为什么先要判断 this 是否存在?

严格模式下,this 若指向全局执行上下文,则会被置为 undefined。

(function(){'use strict';console.log(this);})() // undefined
(function(){console.log(this);})()              // Window
  • this.__extends 有问题吗?

有。如果 this 环境对象中已有 __extends 变量或属性,且其值是 Truthy 的,那 __extends 会被错误地赋值。不过绝大多数情况下 TypeScript 会把控所有代码细节,不会出现冗余的 __extends 变量或属性。

二、

(function () {
  var extendStatics = function (d, b) {
      extendStatics = Object.setPrototypeOf ||
          ({__proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
          function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
      return extendStatics(d, b);
  };

  // 返回匿名函数,赋给 __extends,为方便表述,记该匿名函数为 A 函数
  return function (d, b) {
      extendStatics(d, b);
      function __() { this.constructor = d;}
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  };
})()

嗯。。。这段代码读起来没那么复杂,但是讲解起来比较麻烦,我们来慢慢看。

主要有 10 点:

第 0 点

最外层是一个 IIFE(Immediately Invoked Function Expression,即时调用的函数表达式),函数体内声明了 extendStatics 函数,根据名字来看是用来继承 class 的静态属性和方法。

var extendStatics = function (d, b) {
    extendStatics = Object.setPrototypeOf ||
        ({__proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
    return extendStatics(d, b);
};

这是一个 function expression,声明变量 extendStatics 为一个函数,参数 d 为子类,b 为父类。

函数体中将 extendStatics 重新赋值为一个新的函数 (该函数将子类的 [[Prototype]] 赋值为父类构造函数) 并随后调用了一次,重新赋值的目的:

  • 这其实是一个用完即毁的初始化函数,一方面实现了 extendStatics 的初始化,且将初始化语句限制在函数作用域内。另一方面只要调用一次后,变量 extendStatics 被指向了另外一个函数引用,没有变量再保持对原来初始化函数的引用,这样无论是引用计数还是标记清除,失去了可达性的初始化函数所占的内存可以被引擎快速回收。

来具体看一下初始化过程:

<1> 方案一,如果当前环境支持 Object.setPrototypeOf 则直接将其赋给 extendStatics,否则进行下一步判断;

<2> 方案二,判断 ({ __proto__: [] }) instanceof Array,如果 true,将 function (d, b) { d.__proto__ = b; }) 赋给 extendStatics,否则进行下一步判断;

这里以 V instanceof F 为例,额外讲一下 instanceof,不做实验,一切以标准、规范和引擎的实现为第一优先级:

ECMA-262 标准介绍 instanceof 操作符instanceof 操作符会返回 B 内部 [[HasInstance]] 方法以 A 为参数的调用结果;

ECMA-262 标准介绍 HasInstance:(1)12.10.4Runtime Semantics: InstanceofOperator ( V, target ) ,(2)19.2.3.6Function.prototype [ @@hasInstance ] ( V ) ,(3)6.1.5.1 Well-Known Symbols 表格的第二行

V8 对 ES6 #sec-function.prototype-@@hasinstance 的实现 和对 OrdinaryHasInstance 的实现;

总结一下,求值表达式 V instanceof F 时,会计算 F 内部 [[HasInstance]] 方法以 V 为参数的调用结果,F 内部 [[HasInstance]] 方法是通过 Symbol.hasInstance 部署实现的,所以 V instanceof F 等同于调用 F[Symbol.hasInstance](V)

Symbol.hasInstance 会按固定的顺序检测:

① 如果 V 不是一个 object,返回 false;

② 计 OF.prototype

③ 如果 typeof O !== 'object',抛出 TypeError 异常;

④ 循环:a,令 V 值为 V[[Prototype]] 属性值,即 V = V.__proto__;b,如果 V === null,返回 false;c,如果 OV 是对同一对象的引用,返回 true.

ok,讲解完成,我们再回到 ({__proto__: [] }) instanceof Array,按照上面的逻辑显然:

Array[Symbol.hasInstance]({__proto__:[]})

即计算

({__proto__: [] }).__proto__ === Array.prototype // false

这一步,未满足跳出循环条件,进入下一步循环,计算

({__proto__: [] }).__proto__.__proto__ === Array.prototype // true

返回 true,循环结束,计算完成。

结束 instanceof 的讲解,让我们把思绪拉回至初始化函数的步骤 <2>:

 ({__proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; })

这一步是为了检测当前环境是否允许直接通过字面量直接设置对象的 __proty__ aka [[Prototype]],如果允许,则将 extendsStatics 直接定义为右侧函数,该函数将子类 __proto__ 指向父类的构造函数;如不允许,进入最终方案。

<3> 最终方案,将静态属性一一复制过去,用 hasOwnProperty 过滤掉原型链(__proto__)上的属性。

需要注意的是,前两种方案都是把静态属性所在的对象(即父类的构造函数)委托至子类(的构造函数的 __proto__ 属性)上,而第三种方案是把父类本身的构造函数上的静态属性一一复制到子类的构造函数本身。

第 10 点

继承函数本体

  // 返回匿名函数,赋给 __extends,为方便表述,记该匿名函数为 A 函数
  return function (d, b) {
      extendStatics(d, b);
      function __() { this.constructor = d;}
      d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
  };

这个函数主要实现了:调用 extendStatics 继承静态属性,然后在父类为 null 时直接使用 Object.create 实现继承,在父类不为 null 时使用空函数对象 __ 作为中介继承成员属性,并修正 constructor

为什么要用空函数对象做中介?

// 有中介
d.prototype -> new __()
(new __()).__proto__ -> __.prototype -> b.prototype
即实现了 d.prototype.__proto__ -> __.prototype -> b.prototype

// 无中介 (还有其他无中介的方法,请参考阮一峰的文章,此处只举例最基础的一种)
d.prototype -> new b()
(new b()).__proto__ -> b.prototype
即实现了 d.prototype.__proto__ -> b.prototype

首先两者都可以实现对成员属性和函数的继承,且父类 prototype 一旦更新,子类都会即时受到影响。
有中介的 ** 优势 ** 在于:只需 new 空函数,不需 new 父类,一定程度省了内存。

不过无论是那种方式,只要直接修改子类的 prototype 引用都会影响 constructor,对语义和 JS 的一些内部实现(不包括 instanceof,见上)产生干扰。因此需要 d.prototype.constructor = d,这就是 this.constructor = d; 起的作用。

三、

至此 __extends 函数已分析完,来看一下使用:

var Foo = /** @class */ (function () {
    function Foo() {
        this.foo = 1;
    }
    Foo.prototype.add1 = function (v) {
        return 1 + v;
    };
    Foo.value = 1;
    return Foo;
}());
var Bar = /** @class */ (function (_super) {
    __extends(Bar, _super);
    function Bar() {
        var _this = _super.call(this) || this;
        console.log(_this.foo);
        return _this;
    }
    Bar.subValue = 2;
    return Bar;
}(Foo));

发现除了先调用 __extends 外,还在子类的构造函数内调用父类的构造函数来复用父类的一些逻辑(事实上,如果不调用 super,TypeScript 会直接在静态分析时抛出错误: 派生类的构造函数必须包含 "super" 调用。)。这里也处理了构造函数的返回值问题。

总结

复用:

  1. 静态属性及方法

    • Object.setPrototypeOf

    • function (d, b) { d.__proto__ = b; })

    • function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }

      依次优雅降级。

  2. 成员属性及方法

    通过一个空函数对象增加了一层原型链委托节点。

  3. 父类构造函数

    在声明 constructor 的子类,TypeScript 会强制其调用 super 来调用父类构造函数的逻辑。

需要注意的点

  1. 更改 prototype 的引用需要修正 constructor

  2. 原则上 class 一旦被声明就不应该被增删属性,TypeScript 没有做出源码级别的限制,但是会在静态分析时直接报错。限制了灵活性,增加了规范性,各有利弊。

评价:精巧而严谨

大量使用 IIFE 限制一些语句及变量的生效范围

大量使用括号,&&,||,逗号运算符精简表达式语句

精致的用后即焚初始化函数,减少了变量和内存的冗余

多方案的替换使程序在不同环境中保持健壮

关于闭包(上篇)

date: 2018-08-01

为什么写这篇文章:网上关于闭包的解释五花八门,很多人自己往往也未清楚闭包,就尝试用蹩脚的语言去描述它,而闭包是一个相对抽象的、跨越语言的概念,网上的这些说法往往夹带了JS的私货。所以本篇是我整理的三点关于闭包的权威资料,并没有自己的私货。下面的资料有一些有趣的分歧,如果你发现了并且有兴趣与我探讨一下,欢迎联系我。

一、MDN

英文版:

Closures are functions that refer to independent (free) variables (variables that are used locally, but defined in an enclosing scope). In other words, these functions 'remember' the environment in which they were created.

中文版:

Closures (闭包)是使用被作用域封闭的变量,函数,闭包等执行的一个函数的作用域。通常我们用和其相应的函数来指代这些作用域。(可以访问独立数据的函数)

闭包是指这样的作用域,它包含有一个函数,这个函数可以调用被这个作用域所封闭的变量、函数或者闭包等内容。通常我们通过闭包所对应的函数来获得对闭包的访问。

二、IBMdeveloperworks

闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。对闭包的具体定义有很多种说法,这些说法大体可以分为两类:

一种说法认为闭包是符合一定条件的函数,比如参考资源中这样定义闭包:闭包是在其词法上下文中引用了自由变量(注1)的函数。

另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。比如参考资源中就有这样的的定义:在实现深约束(注2)时,需要创建一个能显式表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。

这两种定义在某种意义上是对立的,一个认为闭包是函数,另一个认为闭包是函数和引用环境组成的整体。虽然有些咬文嚼字,但可以肯定第二种说法更确切。闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要把引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:

函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。

函数可以嵌套定义,即在一个函数内部可以定义另一个函数。

三丶历史上闭包的第一次定义

闭包这个概念第一次出现在1964年的《The Computer Journal》上,由P. J. Landin在《The mechanical evaluation of expressions》一文中提出了applicative expression和closure的概念。

文中AE的概念定义如下:

We are, therefore, interested in a class of expressions about any one of which it is appropriate to ask the following questions:

Q1. Is it an identifier? If so, what identifier?

Q2. Is it a λ-expression? If so, what identifier or identifiers constitute its bound variable part and in what arrangement? Also what is the expression constituting its λ-body?

Q3. Is it an operator/operand combination? If so, what is the expression constituting its operator? Also what is the expression constituting its operand?

We call these expressions applicative expressions (AEs).

在AE的基础上,闭包定义为:

Also we represent the value of a λ-expression by a bundle of information called a "closure", comprising the λ-expression and the environment relative to which it was evaluated. We must therefore arrange that such a bundle is correctly interpreted whenever it has to be applied to some argument. More precisely:

a closure has an environment part which is a list whose two items are:

(1) an environment

(2) an identifier or list of identifiers

and a control part which consists of a list whose sole item is an AE.

结语

本篇中一共摘取了三点资料,前两点中闭包都是跟JS有少许混杂的,或者说跟lexical scope(词法作用域)少许混杂的。想要认识闭包的本质,还需要理解提出闭包的这篇的论文。所以下篇我会研读一下《The mechanical evaluation of expressions》中纯粹的 Closure,敬请期待。

ReactNative 中使用 EOS

date: 2018-11-10

最近遇到了要在 ReactNative 中使用 eosjs 的需求,官方只提供了 node 环境(CommonJS)和 browser 环境(UMD)的包,而 ReactNative 的模块系统则是 node-haste,一个类似 CommonJS 的模块系统。
写篇文章记录一下问题的解决过程,文章内不再赘述 CommonJS AMD UMD 等模块系统的区别和应用。

背景

EOS,可以理解为 Enterprise Operation System,即为商用分布式应用设计的一款区块链操作系统。EOS 是引入的一种新的区块链架构,旨在实现分布式应用的性能扩展,被称为区块链 3.0。

EOS 的核心组成是 nodeos 和 cleos。nodeos 是运行在服务端的区块链节点组件,是 EOSIO 系统的核心进程,可以通过它运行一个节点。nodeos 运行后会暴露出一系列 http 接口,官方称之为 rpc API,可以通过其进行查询及 push transaction 等操作。cleos 是对链进行操作的命令行工具,本质上也是在调用 nodeos 暴露出来的 API,但功能更丰富,可以进行管理钱包、创建账户等敏感性操作。cleos 可以通过指定链的 API 地址来对不同的链进行操作,这更说明 cleos 本质上调用了 nodeos 暴露的 API。

CRUD,除了检索外,所有涉及状态的变更都是由 action 完成的,action 和 contract 在 EOS 中发挥着重要的角色。DAPP 的重中之重是逻辑的编织和逻辑的调用,前者通过编写 contract 丰富 action 的种类完成,后者通过在 DAPP 的 client 使用 EOS 的 SDK 发起各种 action 完成。

EOS SDK

目前,EOS 官方提供的支持度最高的是 JavaScript 版本的 SDK,eosjs。eosjs 主要由两个子包,eosjs-api 和 eosjs-ecc 组成,eosjs-api 负责 http api 调用的部分 (主要是 GET 的部分),eosjs-ecc 负责加密和签名的部分。NPM 仓库中的 eosjs 只能在 node 环境中使用,它使用了 CommonJS 格式,而且它的核心加密模块 eosjs-ecc 使用了大量 node 的 built-in module,例如 buffer、assert 和 crypto。所以不经过处理,eosjs 只能在 node 环境中运行。

然而 eosjs 作为开发 DAPP 和钱包的必需 sdk,为保证用户的数据未被篡改,私钥未被窃取,eosjs 一定要有在客户端中运行的能力。因次随着大量 web 开发者向 eosjs 发出 feature request,eosjs 终于支持了在浏览器中运行,查看其 package.json:

  "scripts": {
    "build_browser": "browserify -o lib/eos.js -s Eos lib/index.js",
  }

其思路大致是利用 browserify 将可以在 node 环境中运行 CommonJS 包编译成可以在浏览器环境中运行的 UMD 包(当然,UMD 包也可以在 node 环境中使用,它同时兼容 AMD 和 CommonJS)。这行命令意思是以 lib/index.js 为入口文件,编译生成 lib/eosjs, 通过执行 browserify -h:

$ browserify -h
Usage: browserify [entry files] {OPTIONS}

Standard Options:
  --standalone -s  Generate a UMD bundle for the supplied export name.
                   This bundle works with other module systems and sets the name
                   given as a window global if no module system is found.

可以了解到 -s 命令指定了模块格式为 UMD,模块名为 Eos

尝试在 ReactNative 中使用 eosjs

在正式讨论 ReactNative 中运行 eosjs 前,先聊几个点:

1.browserify 作为模块打包器,它可以将 node 的 built-in module 和 native module 替换成 polyfill,以让程序在浏览器中得以运行。打包后,所有模块用 key 为 module id 的 plain object 存储,每个模块变成了形如

function (require, module, exports) {...}

的函数,browserify 保留了 require 的函数名,并且重写了 require 函数:

// 需要上下文
function (r) {
          var n = e[i][1][r];
          return o(n || r)
}

原来模块代码中的 require 不再调用 node 内置的 require 函数,而是调用 browserify 实现的 require 函数。

举个例子:

// src/foo.js
var crypto = require('crypto')
module.exports = {
  hash: crypto.createHash
}

// src/index.js
var hash = require('./foo').hash

module.exports = {
  content: hash('md5').update('jader').digest('hex')
}

执行 browserify index.js -o dist/index.js

// dist/index.js
// ... 2 万多行的 polyfill
},{}],154:[function(require,module,exports){
var crypto = require('crypto')

module.exports = {
  hash: crypto.createHash
}
},{"crypto":55}],155:[function(require,module,exports){
var hash = require('./foo').hash

module.exports = {
  content: hash('md5').update('jader').digest('hex')
}
},{"./foo":154}]},{},[155])(155)
});

可以看出,browserify 生成的模块依赖还是比较清晰的,一个 plain object,key 是 module id, value 是一个数组,数组第一位是用函数包起来的元模块代码,调用时传入 browserify 自己实现的 require 函数。数组第二位记录该模块的依赖模块名称及 id,自实现的 require 函数会根据这个字段去加载对应的模块代码。

2.ReactNative 的打包工具既不是 webpack,也不是 gulp,而是自己造的轮子——metro。ReactNative 以 metro 作为打包工具,以 node-haste 作为模块加载方式。

3.ReactNative 不支持动态 require。何为动态 require?

// 不支持
const foo = './src/index.html'
const index = require(foo)

// 不支持
const foo = './src'
const index = require(`${foo}/index.html`)

// 支持
const index = require('./src/index.html')

在 ReactNative 中,打包发生在运行前,而不是运行时。packager 将代码视为文本进行静态分析,并调用 metro 自己实现的 require 函数去处理依赖,因此 require 函数自然无法理解变量参数。

综合以上几点,聊聊我对 eosjs 的使用过程。

一开始,

yarn add eosjs

import Eos from 'eosjs'

react-native run-ios

// 出错
unable to resolve module 'crypto' from ...

想了下,很明显是因为 eosjs 引用了 node built-in module,所以不能被解析打包。去 Github issue 区查了下相关问题,发现 eosjs 对浏览器和 ReactNative 的支持还不完善。
当时业务工期比较紧,而 eosjs-api 不存在平台局限性又能满足业务需要,所以直接用了这个包。

后来,业务需要用到 eosjs-ecc 的部分,又重新去思考如何在 ReactNative 中使用 eosjs。秉承着 “not invented here” 和“不重复造轮子的原则”,我先去社区里找解决方案。巧的是,在这一段时间里,eosjs 出现了 broken change,版本号从 16.0.9 跳到了 20.0.0,而且就在 1 天前社区有人 fork 了新版本的 eosjs-ecc 和 eosjs 进行修改,提供了 eosjs-ecc-rn 和 eosjs-rn 两个 ReactNative 专用包。查 git log 得知,他将两个包中依赖的 node built-in module 替换成了第三方的 polyfill,简单粗暴。开心地试一下,结果报错:

Reference Error: Proxy is not defined

很明显,这是因为 ReactNative 的 runtime 不支持 Proxy,而且 fork 代码的那个哥们还没来得及踩这个坑。

咋办?翻源码。

...
// The size, in bytes, of a word.
var word_size = 4;
...
var X = new Array(t).fill(undefined).map(function(_, i) {
    return new Proxy(new DataView(padded,i * block_size,block_size),{
        get: function get(block_view, j) {
            return block_view.getUint32(j * word_size, true // Little-endian
            );
        }
    });
});
...
// Message word selectors.
var r = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13];
...
RIPEMD160.add_modulo32(A, RIPEMD160.f(j, B, C, D), X[i][r[j]], RIPEMD160.K(j))

所有相关代码都摘在上面了,幸运的是,用到 Proxy 的地方只有一处,而且只拦截了 get 用来在读取值时做实时计算。让人不得不吐槽的是,作为一个底层计算库,这么一个简单的功能明明可以用预计算或者 defineProperty 实现,为什么非要用无法被编译、没有完美 polyfill 的 Proxy 实现呢?

好吧,我来改。

var X = new Array(t).fill(undefined).map(function(_, i) {
    // dataView 只提供 getUint32 方法
    var dataView = new DataView(padded,i * block_size,block_size)
    return Array.from({
        length: 16
    }, (_,k)=>dataView.getUint32(Number(k) * word_size, true))
});

本地改完试一下,编译打包运行,没问题,顺手发了个 Pull Request。第二天起床一看,被顺顺利利 merge 了,还被感谢了一番,开心 XD。

尾注

其实这段过程也蛮曲折的,为了行文流畅,我把费尽心思调研出来的要点放在了前面。其实社区中 ReactNative Packager (即 Metro) 和 node-haste 的资料并不多,EOS 的 ReactNative 社区也可谓是贫瘠,很多坑需要自己去踩。比如说最后 Proxy 的问题,开发阶段使用模拟器还好好的,打包后在真机运行时却会白屏,查看错误信息才发现是 Proxy 的问题,根源是在真机中,代码运行在 JavaScriptCore 中,也就是 Safari 和国产一众小程序在 iOS 的引擎,而开发调试中,代码运行在 blink/v8 中,Proxy 又是不能被编译和没有完美 polyfill 的,所以 ReactNative 并不支持 Proxy,我查看了 JavaScriptCore 的相关 Proxy feature request issue,发现 JavaScriptCore 对移动端适配的版本对 Proxy 的支持还是遥遥无期,也就是说,国产一众小程序也无法使用 Proxy 了。社区中 Proxy polyfill 的最好版本是 GoogleChrome/proxy-polyfill,但是它对数组的处理跟原生的 Proxy 相比还是有差异,我曾尝试使用这一版本的 polyfill,结果以失败告终。

还有一点有趣的是,在最新版本的 ReactNative 中,新的 metro 可以对 browserify 输出的文件进行正确的解析和打包,不会发生怪异的 require 替换。但是 browserify 对一些 node built-in/native modules 的 polyfill 会嫌弃 JavaScriptCore 支持的特性太少而无法运行,又是一记讽刺。metro 的提升缓慢,又不与 webpack 共享生态,node-haste 业已处于半废弃的状态,希望 ReactNative 的团队在将来的重构中重新思考和设计模块加载和打包方式。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.