iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 23
1
Modern Web

高效 Coding 術:Angular Schematics 實戰三十天系列 第 23

[高效 Coding 術:Angular Schematics 實戰三十天] Day22 - 與 React 共舞

練習目標

React 有個套件叫做 reactstrap ,它可以讓開發者在 React 專案裡很方便的去使用 Bootstrap 的元件。

而安裝 reactstrap 的步驟大致如下:

  1. 安裝 bootstrapreactstrap@types/reactstrap 這三個套件。
  2. /src/index.tsx 裡引入 bootstrap.min.css
  3. 新增使用範例。

因此,我們今天的練習目標就是:透過 Schematics 來濃縮以上程序,並幫它直接新增一個使用範例

開始囉!

準備

不管你原本有沒有 React 專案,都建議先在終端機輸入以下指令來建立新專案,因為我們後續的練習都是針對該專案而開發:

npx create-react-app react-with-schematics --typescript

安裝完成後可以將專案啟動,如果有看到以下畫面代表專案有正常建立:

Imgur

此外,如果你也還沒有安裝 Schematics CLI 的話,也記得請先輸入以下指令安裝:

npm instal @angular-devkit/schematics-cli -g

接著輸入以下指令建立一個新的 Schematics 專案:

schematics blank schematics-for-react

修改一下 /src/collection.json 裡的設置:

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "add-reactstrap": {
      "description": "Add 'reactstrap'",
      "factory": "./add-reactstrap"
    }
  }
}

上述程式碼中,筆者將原本的 schematics-for-react 相關字串都改為 add-reactstrap ,因為筆者想讓大家之後可以沿用此專案來開發自己的 Schematic ,所以改了個較適當的名字。

另外也調整了 description 的敘述與 factory 的路徑,因此需要同步將 /src/schematics-for-react 的資料夾名稱改為 /src/add-reactstrap ,並將 /src/add-reactstrap/index.ts 裡的程式碼改成:

import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';

export default function (_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    return tree;
  };
}

同時,因為有調整 schematic 名稱,/src/add-reactstrap/index_spec.ts 裡的程式碼也要一併調整:

describe('add-reactstrap', () => {
  it('works', () => {
    const runner = new SchematicTestRunner('schematics', collectionPath);
    const tree = runner.runSchematic('add-reactstrap', {}, Tree.empty());
    expect(tree.files).toEqual([]);
  });
});

接著我們就可以輸入以下指令來驗證目前的程式碼有沒有任何問題:

# run test
npm test 

# run schematic
schematics .:add-reactstrap

請注意,如果沒有先輸入 npm test 或者是 npm run build,直接使用 schematics .:add-bootstrap-vue 是會報錯的噢!

如果都沒有出現錯誤,就可以開始撰寫程式碼囉!

第一步:安裝相依的套件

首先,第一步是要透過 schematic 來安裝相依的 bootstrapreactstrap@types/reactstrap 這三個套件,而在開始撰寫程式碼之前,一樣先寫測試。

首先先在 /src/add-reactstrap 的資料夾裡新增一個檔名為 react-package.json 的 JSON 檔,內容如下:

{
  "name": "react-package-for-test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.10.2",
    "react-dom": "^16.10.2",
    "react-scripts": "3.2.0"
  }
}

而為了更方便我們讀取 JSON 檔,所以先打開專案根目錄裡的 tsconfig.json ,並在 compilerOptions 裡加上這兩個屬性:

{
  "compilerOptions": {
    "//": "略",
    "resolveJsonModule": true,
    "esModuleInterop": true
  }
}

再打開 /src/add-reactstrap/index_spec.ts ,輸入以下內容:

import { Tree } from '@angular-devkit/schematics';
import { SchematicTestRunner } from '@angular-devkit/schematics/testing';

import packageJson from './react-package.json';

import * as path from 'path';

const collectionPath = path.join(__dirname, '../collection.json');

describe('add-reactstrap', () => {
  it('works', () => {
  
    const appTree = Tree.empty();
    appTree.create('./package.json', JSON.stringify(packageJson));

    const runner = new SchematicTestRunner('schematics', collectionPath);
    tree = runner.runSchematic('add-reactstrap', {}, appTree);

    const json = JSON.parse(tree.readContent('/package.json'));
    const dependencies = json.dependencies;
    expect(dependencies['bootstrap']).toBeDefined();
    expect(dependencies['reactstrap']).toBeDefined();
    expect(dependencies['@types/reactstrap']).toBeDefined();

    expect(runner.tasks.some(task => task.name === 'node-package')).toBe(true);
  });
});

如何驗證是否有安裝相依套件的部分,之前有在 Day14 - 實戰 ng add 的文章裡有寫過,不知道或忘記的邦友可以回去看看。

輸入以下指令驗證:

npm test

結果:

Imgur

因為我們先寫測試,所以沒有通過測試是正常的!

然後打開 /src/add-reactstrap/index.ts 來實作功能:

import { Rule, SchematicContext, Tree, SchematicsException } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';

export default function (_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {

    const packageFileName = '/package.json';

    // 檢查專案內有沒有 package.json ,沒有的話直接拋出 Error
    if ( !tree.exists(packageFileName) ) {
      throw new SchematicsException(`'package.json' doesn't exist.`);
    }

    // 讀取 package.json 檔,並轉成 JSON
    const sourceText = tree.read(packageFileName)!.toString('utf-8');
    const json = JSON.parse(sourceText);

    // 防呆處理
    if ( !json.dependencies ) {
      json.dependencies = {};
    }

    // 將要安裝的套件名稱先加進 package.json 之中
    const packages = ['bootstrap', 'reactstrap', '@types/reactstrap'];
    packages.forEach((packageName) => {
      
      if ( !json.dependencies[packageName] ) {
        json.dependencies[packageName] = '*';
        json.dependencies = sortObjectByKeys(json.dependencies);
      }

      tree.overwrite('/package.json', JSON.stringify(json, null, 2));

    });

    // 實際安裝套件
    _context.addTask(
      new NodePackageInstallTask({
        packageName: packages.join(' ')
      })
    );

    return tree;
  };
}

// 按照 package 的名稱排序
function sortObjectByKeys(obj: any): object {
  return Object.keys(obj).sort().reduce((result, key) => (result[key] = obj[key]) && result, {} as any);
}

最後輸入 npm test 驗證:

Imgur

第二步:引入樣式檔

這步其實非常的簡單,不過我們一樣先來準備測試的東西。

先在 /src/add-reactstrap 底下新增一個 react-index.ts 檔,檔案內容如下:

export const reactIndexContent = `
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

serviceWorker.unregister();
`;

再打開 /src/add-reactstrap/index_spec.ts ,先引入剛剛的 react-index.ts

import { reactIndexContent } from './react-index';

接著新增以下程式碼:

describe('add-reactstrap', () => {
  it('works', () => {
    // 略
    appTree.create('./src/index.tsx', reactIndexContent);
    // 略
    const mainContent = tree.readContent('/src/index.tsx');
    expect(mainContent).toContain(`import 'bootstrap/dist/css/bootstrap.min.css';`);
    // 略
  });
});

然後再打開 /src/add-reactstrap/index.ts ,先引入 TypeScript Compiler API 與 TSQuery :

import { tsquery } from '@phenomnomnominal/tsquery';
import * as ts from 'typescript';

不知道怎麼使用 TypeScript Compiler API 與 TSQuery 的邦友可以參考 Day10Day20 的文章。

再新增以下程式碼:

export default function (_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    // 略
  
    const indexTsxFileName = '/src/index.tsx';

    if ( !tree.exists(indexTsxFileName) ) {
      throw new SchematicsException(`'/src/index.tsx' doesn't exist.`);
    }
    
    const indexTsxAst = tsquery.ast(tree.read(indexTsxFileName)!.toString(), '', ts.ScriptKind.TSX);
    const lastImportDeclaration = tsquery(indexTsxAst, 'ImportDeclaration').pop()! as ts.ImportDeclaration;
    
    const indexTsxRecorder = tree.beginUpdate(indexTsxFileName);
    indexTsxRecorder.insertLeft(lastImportDeclaration.end, `\nimport 'bootstrap/dist/css/bootstrap.min.css';`);
    tree.commitUpdate(indexTsxRecorder);
  
    return tree;
  };
}

寫完之後一樣輸入 npm test 來驗證結果:

Imgur

第三步:新增使用範例

最後一步則是要透過 Schematic 來新增使用範例。

老規矩,先準備測試要用的東西。

先在 /src/add-reactstrap 底下新增一個 react-app.ts ,內容如下:

export const reactAppContent = `
import React from 'react';
import logo from './logo.svg';
import './App.css';

const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
`;

再打開 /src/add-reactstrap/index_spec.ts ,先引入剛剛的 react-app.ts

import { reactAppContent } from './react-app';

接著新增以下測試程式碼:

describe('add-reactstrap', () => {
  it('works', () => {
    // 略
    appTree.create('./src/App.tsx', reactAppContent);
    // 略
    const appContent = tree.readContent('/src/App.tsx');
    expect(appContent).toMatch(/import.*Alert.*from 'reactstrap'/);
    expect(appContent).toMatch(/<Alert color="success">reactstrap installed successfully!\r?\n\s+<span role="img" aria-label="hooray">\?<\/span>\r?\n\s+<\/Alert>/m);
    // 略
  });
});

然後再打開 /src/add-reactstrap/index.ts ,新增以下程式碼:

export default function (_options: any): Rule {
  return (tree: Tree, _context: SchematicContext) => {
    // 略
  
    const appTsxFileName = '/src/App.tsx';

    if ( !tree.exists(appTsxFileName) ) {
      throw new SchematicsException(`'/src/App.tsx' doesn't exist.`);
    }
    
    const appTsxAst = tsquery.ast(tree.read(appTsxFileName)!.toString(), '', ts.ScriptKind.TSX);
    const appLastImportDeclaration = tsquery(appTsxAst, 'ImportDeclaration').pop()! as ts.ImportDeclaration;
    const jsxClosingElement = tsquery(appTsxAst, 'VariableDeclaration[name.name="App"] JsxClosingElement[tagName.escapedText="a"]').pop()! as ts.JsxClosingElement;
    let toInsert = `\n        <Alert color="success">reactstrap installed successfully!`;
    toInsert += `\n          <span role="img" aria-label="hooray">?</span>`;
    toInsert += `\n        <\/Alert>`;

    const appTsxRecorder = tree.beginUpdate(appTsxFileName);
    appTsxRecorder.insertLeft(appLastImportDeclaration.end, `\nimport { Alert } from 'reactstrap';`);
    appTsxRecorder.insertLeft(jsxClosingElement.end, toInsert);
    tree.commitUpdate(appTsxRecorder);
  
    return tree;
  };
}

小提醒: TSQuery Playground 目前不支援 JSX 格式,因此只能使用 TypeScript AST Viewer 來輔助開發。

寫完之後輸入 npm test 來驗證結果:

Imgur

至此,我們總算初步寫好了我們第一個給 React 專案所使用的 Schematic 了!!

接下來,就是帶著它上戰場了!

在 React 專案裡使用尚未發佈的 Schematic

還記得我們一開始有用以下指令建立了一個新專案嗎?

npx create-react-app react-with-schematics --typescript

現在可以先打開它,並且在該專案的根目錄底下輸入以下指令:

schematics /path/to/schematics-for-react/src/collection.json:add-reactstrap --dry-run=false

等到程序都跑完之後,再輸入以下指令啟動專案:

yarn start

結果:

Imgur

在 React 專案裡使用尚未發佈的 Schematic

筆者有把 schematics-for-react 這個專案發佈到 npm 上面,所以當今天大家想要在 React 專案裡使用已經發佈的 Schematics 時,只要先輸入以下指令安裝它:

yarn add schematics-for-react

安裝完畢後,再輸入以下指令來使用它:

schematics schematics-for-react:add-reactstrap

一樣可以使用!

本日結語

今天的程式碼:https://github.com/leochen0818/schematics-for-react

希望大家可以透過今天的文章學習到:

  • 練習開發給 React 專案使用的 Schematics 。
  • 如何在 React 專案使用 Schematics 。

React 的專案結構較為彈性,所以大家在開發要給 React 專案使用的 Schematics 時,要特別留意。

明天筆者將介紹如何開發給 Vue 專案使用的 Schematics ,敬請期待!

參考資料


上一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day21 - 超好用工具分享之 Schematics Snippets 和 TypeScript AST Viewer
下一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day23 - 與 Vue 共舞
系列文
高效 Coding 術:Angular Schematics 實戰三十天32

尚未有邦友留言

立即登入留言