今天的練習基本上跟前一篇非常類似,不過就只是將主角從 React 換成了 Vue ,大體上沒有太大的改變。
但是魔鬼藏在細節裡,且為了不是每天文章都會仔細閱讀的邦友,所以筆者還是會照著前篇文章的步驟與結構再闡述一次。
而有看過前一篇文章的朋友,則不一定要完全按照文章的方式做,可以自己試著玩玩看,或許會有更多的收獲噢!
Vue 有個套件叫做 bootstrap-vue ,它可以讓開發者在 Vue 專案裡使用 Bootstrap 的元件變得更方便。
而安裝 bootstrap-vue 的步驟大致如下:
bootstrap
、 bootstrap-vue
這兩個個套件。/src/main.js
裡引入 bootstrap.css
、 bootstrap-vue.css
與 BootstrapVue
,並套用它。因此,我們今天的練習目標就是:透過 Schematics 來濃縮以上程序,並幫它直接新增一個使用範例。
開始囉!
如果你還沒有安裝過 Vue CLI 的話,可以先輸入以下指令安裝:
npm install @vue/cli -g
而不管你原本有沒有 Vue 專案,都建議先在終端機輸入以下指令來建立新專案,因為我們後續的練習都是針對該專案而開發:
vue create vue-with-schematics
筆者在安裝過程中所使用的選項為:
安裝完成後可以將專案啟動,如果有看到以下畫面代表專案有正常建立:
此外,如果你也還沒有安裝 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 來安裝相依的 bootstrap
與 bootstrap-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
結果:
因為我們先寫測試,所以沒有通過測試是正常的!
然後打開 /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
驗證:
這步其實非常的簡單,不過我們一樣先來準備測試的東西。
先在 /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 的邦友可以參考 Day10 與 Day20 的文章。
再新增以下程式碼:
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
來驗證結果:
最後一步則是要透過 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
來驗證結果:
至此,我們總算初步寫好了我們第一個給 Vue 專案所使用的 Schematic 了!!
接下來,就是帶著它上戰場了!
還記得我們一開始有用以下指令建立了一個新專案嗎?
vue create vue-with-schematics
現在可以先打開它,並且在該專案的根目錄底下輸入以下指令:
schematics /path/to/schematics-for-vue/src/collection.json:add-bootstrap-vue --dry-run=false
等到程序都跑完之後,再輸入以下指令啟動專案:
yarn serve
結果:
筆者有把 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 的專案結構較為彈性,所以大家在開發要給 Vue 專案使用的 Schematics 時,一樣要特別留意。