iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 24
1
Modern Web

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

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

今天的練習基本上跟前一篇非常類似,不過就只是將主角從 React 換成了 Vue ,大體上沒有太大的改變。

但是魔鬼藏在細節裡,且為了不是每天文章都會仔細閱讀的邦友,所以筆者還是會照著前篇文章的步驟與結構再闡述一次。

而有看過前一篇文章的朋友,則不一定要完全按照文章的方式做,可以自己試著玩玩看,或許會有更多的收獲噢!

練習目標

Vue 有個套件叫做 bootstrap-vue ,它可以讓開發者在 Vue 專案裡使用 Bootstrap 的元件變得更方便。

而安裝 bootstrap-vue 的步驟大致如下:

  1. 安裝 bootstrapbootstrap-vue 這兩個個套件。
  2. /src/main.js 裡引入 bootstrap.cssbootstrap-vue.cssBootstrapVue ,並套用它。
  3. 新增使用範例。

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

開始囉!

準備

如果你還沒有安裝過 Vue CLI 的話,可以先輸入以下指令安裝:

npm install @vue/cli -g

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

vue create vue-with-schematics

筆者在安裝過程中所使用的選項為:

Imgur

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

Imgur

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

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

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

schematics blank schematics-for-vue

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

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

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

另外也調整了 description 的敘述與 factory 的路徑,因此需要同步將 /src/schematics-for-vue 的資料夾名稱改為 /src/add-bootstrap-vue ,並將 /src/add-bootstrap-vue/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-bootstrap-vue/index_spec.ts 裡的程式碼也要一併調整:

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

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

# run test
npm test 

# run schematic
schematics .:add-bootstrap-vue

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

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

第一步:安裝相依的套件

首先,第一步是要透過 schematic 來安裝相依的 bootstrapbootstrap-vue 這兩個套件,而在開始撰寫程式碼之前,一樣先寫測試。

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

{
  "name": "vue-package-for-test",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "core-js": "^2.6.5",
    "vue": "^2.6.10"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "^3.11.0",
    "@vue/cli-plugin-eslint": "^3.11.0",
    "@vue/cli-service": "^3.11.0",
    "babel-eslint": "^10.0.1",
    "eslint": "^5.16.0",
    "eslint-plugin-vue": "^5.0.0",
    "vue-template-compiler": "^2.6.10"
  }
}

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

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

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

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

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

import * as path from 'path';

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

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

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

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

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

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

輸入以下指令驗證:

npm test

結果:

Imgur

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

然後打開 /src/add-bootstrap-vue/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', 'bootstrap-vue'];
    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-bootstrap-vue 底下新增一個 vue-main.ts 檔,檔案內容如下:

export const vueMainContent = `
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')
`;

再打開 /src/add-bootstrap-vue/index_spec.ts ,先引入剛剛的 vue-main.ts

import { vueMainContent } from './vue-main';

接著新增以下程式碼:

describe('add-bootstrap-vue', () => {
  it('works', () => {
    // 略
    appTree.create('./src/main.js', vueMainContent);
    // 略
    const mainContent = tree.readContent('/src/main.js');
    expect(mainContent).toContain(`import BootstrapVue from 'bootstrap-vue'`);
    expect(mainContent).toContain(`import 'bootstrap/dist/css/bootstrap.css'`);
    expect(mainContent).toContain(`import 'bootstrap-vue/dist/bootstrap-vue.css'`);
    expect(mainContent).toContain(`Vue.use(BootstrapVue)`);
    // 略
  });
});

然後再打開 /src/add-bootstrap-vue/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 mainJsFileName = '/src/main.js';

    if ( !tree.exists(mainJsFileName) ) {
      throw new SchematicsException(`'/src/main.js' doesn't exist.`);
    }
    
    const mainJsAst = tsquery.ast(tree.read(mainJsFileName)!.toString(), '', ts.ScriptKind.JS);
    const lastImportDeclaration = tsquery(mainJsAst, 'ImportDeclaration').pop()! as ts.ImportDeclaration;
    const binaryExpression = tsquery(mainJsAst, `BinaryExpression[right.kind=${ts.SyntaxKind.FalseKeyword}]`).pop()! as ts.BinaryExpression;
    
    let importString = `\nimport BootstrapVue from 'bootstrap-vue'`;
    importString += `\nimport 'bootstrap/dist/css/bootstrap.css'`;
    importString += `\nimport 'bootstrap-vue/dist/bootstrap-vue.css'`;
    
    const mainJsRecorder = tree.beginUpdate(mainJsFileName);
    mainJsRecorder.insertLeft(lastImportDeclaration.end, importString);
    mainJsRecorder.insertLeft(binaryExpression.getStart(), 'Vue.use(BootstrapVue)\n');
    tree.commitUpdate(mainJsRecorder);
  
    return tree;
  };
}

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

Imgur

第三步:新增使用範例

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

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

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

export const vueAppContent = `
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'app',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
`;

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

import { vueAppContent } from './vue-app';

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

describe('add-bootstrap-vue', () => {
  it('works', () => {
    // 略
    appTree.create('./src/App.vue', vueAppContent);
    // 略
    const appContent = tree.readContent('/src/App.vue');
    expect(appContent).toContain(`<b-alert variant="success" show>Bootstrap Vue installed successfully!</b-alert>`);
    // 略
  });
});

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

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

    if ( !tree.exists(appVueFileName) ) {
      throw new SchematicsException(`'/src/App.Vue' doesn't exist.`);
    }

    const appSource = tree.read(appVueFileName)!.toString();
    const appVueAst = tsquery.ast(appSource, '', ts.ScriptKind.TSX);
    const divOpenNode = tsquery(appVueAst, 'JsxOpeningElement[tagName.escapedText="div"]').pop()! as ts.JsxOpeningElement;
    const imgNode = tsquery(appVueAst, 'JsxOpeningElement[tagName.escapedText="img"]').pop()! as ts.JsxOpeningElement;
    const changeText = appSource.substring(divOpenNode.end, imgNode.end);
    const toInsert = `${changeText.match(/^\r?\n\s*/)![0]}<b-alert variant="success" show>Bootstrap Vue installed successfully!</b-alert>`;
    
    const appVueRecorder = tree.beginUpdate(appVueFileName);
    appVueRecorder.insertLeft(imgNode.end, toInsert);
    tree.commitUpdate(appVueRecorder);
  
    return tree;
  };
}

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

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

Imgur

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

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

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

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

vue create vue-with-schematics

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

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

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

yarn serve

結果:

Imgur

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

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

yarn add schematics-for-vue

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

schematics schematics-for-vue:add-bootstrap-vue

一樣可以使用!

本日結語

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

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

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

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

參考資料


上一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day22 - 與 React 共舞
下一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day24 - Angular Schematics API List (一)
系列文
高效 Coding 術:Angular Schematics 實戰三十天32

尚未有邦友留言

立即登入留言