こんにちは。
こちらは、Qiita
のTypeScript Advent Calendar 2018
の3日目の記事になります。
これまで、特にTypeScript
を使ってこなかったどころか、Webフロントの開発がほぼ初心者の筆者が、突然xlsx.js
というライブラリを使って、Webフロントの開発を行った話をします。
とりあえず動いたよ!というところを話します。そもそも間違ってるよ!というツッコミ大歓迎です。
経緯
Python
のDjango
を使った開発をしていて、フロントエンドでExcel読み込めないかなぁ、という話になりました。
その中で、JavaScript
でExcelを読み込むための、Sheetjs
(ファイル名:xlsx.js
)というものがあることを知りました。
GitHub
ページはこちらです。
もともと、簡単なDOM操作でTypeScript
を導入していたので、その延長線で使えるだろう、と踏んでのことでした。
TypeScript開発環境
今回はPython
のAdvent Calendarではないので詳細は省きますが、TypeScript
自体はNode.js
で導入し、コンパイルはtsc
で行っていました*1。
結果のJavaScript
を、Django
が呼び出すファイルから参照しています。
コーティングにはVisual Studio Code
を利用しています。
xlsx.jsのインストール
これも、npm
経由でインストールできます。
npm install --save xlsx
とすればいいです。
TypeScriptから参照
TypeScript
からxlsx.js
を参照する場合、型定義が用意されています。
js-xlsx/demos/typescript at master · SheetJS/js-xlsx · GitHub
ここに書いてある通り、
import { WorkBook, WorkSheet, utils, Range, CellObject, CellAddress } from "xlsx";
という感じで、必要なモジュールを参照しました。
作ろうとしたもの
使い方に関しては、
や
の記事を参照しました。この場を借りてお礼を申し上げます。
使い方として、結局は
- HTMLのinput要素でファイルを選択する
- 選択されたファイルを検知し、Excelとして開く
- あとはよしなに
というところに帰結するので、Workbook
を取得するところまで、モジュールとして切り出してしまいました。
- event.ts
interface HTMLElementEvent<T extends HTMLElement> extends Event { target: T; } interface ObjectEvent<T extends EventTarget> extends Event { target: T; }
- excel.ts
///<reference path="event.ts"> import * as excel from "xlsx"; /** * Excel読み込み処理 */ export interface IExcelRead { read(xls: excel.WorkBook); } /** * ファイルを選択します。 */ export class FileSelector { private input: HTMLInputElement; private selectedFile: File | null; private reader: IExcelRead; constructor(elem: HTMLInputElement, reader: IExcelRead) { this.input = elem; elem.addEventListener("change", this.fileSelect.bind(this)); this.selectedFile = null; this.reader = reader; } private fileSelect(evt: HTMLElementEvent<HTMLInputElement>) { if (evt.target.files) { this.selectedFile = evt.target.files[0]; this.read(); } } private read() { if (!this.selectedFile) { return; } const reader = new FileReader(); reader.addEventListener("load", this.loadSuccess.bind(this)); reader.readAsArrayBuffer(this.selectedFile); } /** * ファイルロードに成功したら発生 * @param evt イベント対象オブジェクト */ private loadSuccess(evt: ObjectEvent<FileReader>) { const data = evt.target.result; if (!data) { return; } if (typeof data === "string") { return; } const arr = this.handleCodePoints(new Uint8Array(data)); const book = excel.read(btoa(arr), { type: "base64", raw: true }); this.reader.read(book); } // see: https://github.com/mathiasbynens/String.fromCodePoint/issues/1 private handleCodePoints(array: Uint8Array) { var CHUNK_SIZE = 0x8000; // arbitrary number here, not too small, not too big var index = 0; var length = array.length; var result = ''; let slice: Uint8Array; while (index < length) { slice = array.slice(index, Math.min(index + CHUNK_SIZE, length)); // `Math.min` is not really necessary here I think result += String.fromCharCode.apply(null, slice); index += CHUNK_SIZE; } return result; } }
エラーチェックは激甘です。
動かない
こう書いて、コーディング中はIntelliSense
も動作しました。
使う時は、
const input = <HTMLInputElement>document.getElementById("input要素の名前"); const reader = new "IExcelReaderを実装したクラス"(); const f = new FileSelector(input, reader);
という形で使えます。
いざ、tsc
でコンパイルして動かすと、
Uncaught ReferenceError: exports is not defined
というエラーが。
これがなんなのか、まったくわかりませんでした。
tsconfig.json定義
最低限ですが。
{ "compilerOptions": { /* Basic Options */ "target": "es5", "module": "commonjs", "lib": ["es2017", "dom"], } }
そもそもこの定義でimport
が使えるのかわかりませんが…
さんざん悩んだ後
これで合っているかどうかわかりませんが。
- xlsxでは、
index.d.ts
はあるものの、型定義(DefinitelyTyped)は無い - 自分で型定義を書かない限り、
import
からは逃れられない - ビルドはできても、生成された
JavaScript
がエラーになる
という状況だったので、
- まず、
tsc
でビルドしたJavaScript
を用意する - それらを
browserify
を使ってくっつける
という、なんだかごり押しな方法で動きました。
browserify
ですが、
Node.jsのモジュールシステムをブラウザでも利用できるようになる
ものです。
というか、import
等はNode.js
限定の書き方なんですか…?TypeScript
の公式ではいかにも使えます感で書いてありましたが…ES6
からは使えるそうなので、対応待ちでしょうか。
上で書いたexcel.ts
ですが、上記のtsconfig.json
の設定でビルドすると、JavaScript
先頭に以下の記述が付与されます。
- excel.js
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); var excel = __importStar(require("xlsx"));
そうです。require
が現れます。よって、browserify
で結合すれば使えます。
ビルドを自動化したい
browserify
で結合するのはよいのですが、ビルド手順が
tsc
でTypeScript
をJavaScript
に変換browserify
で、指定したJavaScript
だけ結合
となり、毎回手作業はしんどいです。
そこで、package.json
のscripts
に、コマンドを定義してしまいました。こんな感じです。
import.ts
というのは、上記excel.ts
を参照しているファイルです。
{ // 略 "scripts": { "build:ts": "tsc", "build:ts:import": "tsc import.ts tslib/event.ts tslib/excel.ts --target es5 --outDir dist --removecomments --declaration --sourcemap --lib es2015,dom", "build:br:debug": "browserify dist/import.js --debug -o ../static/src/js/imports.js", "build:debug": "npm run build:ts:import && npm run build:br:debug" }, // 略 }
一つのコマンドでは複数のコマンドを実行できないため、npm run
を複数回実行するようにしています。
これで、
TypeScript
をJavaScript
に変換し、一つのフォルダにまとめる
→生成されたJavaScript
に対し、browserify
でrequires
の内容を解決し、一つのファイルにまとめる
という作業が、一つのコマンド(npm run build:debug
コマンド)で実行できます。
あとは、Visual Studio Code
のタスクで呼び出すよう設定すると、実行時にスクリプトのビルドが勝手に行われます。便利。
おわりに
なんか、Excelを読み込んだ話はどこかに行った感があります。
実際、上のような設定をしてビルドすると、Excel読み込みが可能になりました。
最終的には、uglify-es
等を使ってスクリプトを圧縮すれば使えそうです。
…ただ、いろいろ調べると、webpack
で複数ファイルの結合から圧縮まで、全部できそうな雰囲気があります。
いつか試してみたいところです。
途中、説明が飛んだ部分とかある気がするので、ちょいちょい追加します。
余談
筆者の周りはTypeScript
分かる人が誰もいないので、一人で突っ走って何とかしました。
教えてくれる人が欲しい今日この頃です。