LoginSignup
27
21

More than 5 years have passed since last update.

TypeScript2 + webpack2でのcss-loader, file-loader周辺をなんとかする

Last updated at Posted at 2017-02-08

WebpackとTypeScriptの扱いについて、まだ手探りだが、今のところのプラクティスをまとめたい。

css-loader, file-loaderなどで読み込むリソースが型エラーになるのをなんとかする話

webpack + babel + css-loader という構成のところからwebpack + typescriptにしていくにあたって、js, ts以外のファイルの読み込みは、当然そのままだと型がエラーとなる。

これを解決するのに、2点ほど手順が必要だった。

手順1. 読み込み方法を変更

file-loaderなどはmodule.exports = "some-resource"という形で吐き出しされるので、TypeScriptにあわせるのであれば、importの仕方を変える方が良い。
(Webpack2のtree shakingを利用する場合は、元のコードでも壊れず動いてしまうのだが、これについては後述)

// 元のコード
import style from "./foo.css"

これをこうしていく。

import * as style from "./foo.css"
// または
import style = require("./foo.css")

2. typing

型定義をしないとtypescriptは落ちるので行う。
tsconfg.jsonで独自なtypingsディレクトリを用意した上で型定義をすると良さそう。

{
    ...
    "typeRoots" : [
        "typings",
        "node_modules/@types/"
    ]
    ...
}

型定義にはWildcard module declarationという定義方法を使う事で乗り越えられる。

「とりあえずanyでいいから!なんとかしたい!」という場合は、こんな具合。

// typings/resource.d.ts

declare module "*.css"
declare module "*.png"
declare module "*.jpg"

「もうちょっと毛が生えた感じに!」という場合はこのぐらいまでならサクッと定義できる

// typings/resource.d.ts
declare module "*.css" {
  const classes: {[className: string]: string} // css-moduleの結果をstring型のobjectに
  export = classes
  // import style from "./foo.css"で読み込みたいなら下記(後述)
  // export default classes
}

declare module "*.png" {
  const content: string
  export = content
  // import style from "./some.png"で読み込みたいなら下記(後述)
  // export default content
}

この定義はこのへんを参考にした。
* https://github.com/s-panferov/awesome-typescript-loader/issues/146#issuecomment-248808206
* https://github.com/Quramy/typed-css-modules/issues/2#issuecomment-256794347)
* https://github.com/Microsoft/TypeScript/issues/6615#issuecomment-188593420

「もっとちゃんと型定義したい!」という場合は、下記などを導入するのが良いだろう。

webpackでのimport解決のされ方はtsconfig.jsonのcompilerOptions.moduleによって変わるという話

TL;DR
* module: es2015の場合、typescriptはモジュール解決に関与しない。webpackにモジュール解決が移譲されて、babelっぽく扱われる(babelと互換性が維持されるが、ESmoduleの仕様とズレるので、今後どこかで変更されるだろうと思われる)
* module: commonjsだと、typescriptがimport / exportを変換する。互換性は維持されない(正しいESmoduleの仕様に準拠する。babelとの互換性はなくなる)

前提(と疑問)

例えばurl-loaderfile-loaderは、下記のような吐き出しをする

/******/ ([
/* 0 */
/***/ (function(module, exports) {

module.exports = "data:text/plain;base64,aGVsbG8="

/***/ }),

ESmoduleの読み方で言えばimport * as foo from "foo"という読み方でないと読み込めない理屈になる。
しかし、自分の手元で正しく動いていたので、「これは何故だろう?」という疑問が出てきた。

実験

まずこんなTypeScriptファイルを用意

import * as txt1 from "./foo.txt"
import txt2 from "./foo.txt"

console.log("===========")
console.log("import * as txt1")
console.log("=>", txt1)
console.log("===========")
console.log("===========")
console.log("import txt2")
console.log("=>", txt2)
console.log("===========")

Webpack設定を用意。
今回は、外からcompilerOptions.moduleを変更できるようにする。

module.exports = ( env ) => {
  const { moduleType } = env
  return {
    entry: './src/index.ts',
    output: {
      path: `output/${moduleType}`,
      filename: 'bundle.js'
    },
    resolve: {
      extensions: ['.ts', '.js']
    },
    module: {
      rules: [
        { test: /\.txt$/, use: 'url-loader' },
        { test: /\.ts$/, use: [{
          loader: 'ts-loader',
          options: {
            compilerOptions: {
              // tsconfig.jsonのmoduleを上書き
              module: moduleType
            }
          }
        }]},
      ]
    }
  }
}

そしてビルド→実行をする

まずcommonjsの場合

$ webpack --env.moduleType=commonjs
$ node output/commonjs/bundle.js
===========
import * as txt1
=> data:text/plain;base64,aGVsbG8=
===========
===========
import txt2
=> undefined
===========

結果:import txt2 from "./foo.txt"undefinedになる。

次にes2015

$ webpack --env.moduleType=es2015
$ node output/es2015/bundle.js
===========
import * as txt1
=> data:text/plain;base64,aGVsbG8=
===========
===========
import txt2
=> data:text/plain;base64,aGVsbG8=
===========

結果:import * as txt1 from "./foo.txt"import txt2 from "./foo.txt"、どちらもstringが取得できる。

解説

commonjsの場合の話

Typescript(ts-loader)の段階で、import module from "./module"は下記のような感じで変換される

var module_1 = require("./module");
var a = function () {
    module_1["default"]();
};

最終的にこんな感じに変換される。

var txt1 = __webpack_require__(0);
var foo_txt_1 = __webpack_require__(0); // txt2として読み込んだもの
console.log("===========");
console.log("import * as txt1");
console.log("=>", txt1);
console.log("===========");
console.log("===========");
console.log("import txt2");
console.log("=>", foo_txt_1.default); // .defaultにアクセスしてる
console.log("===========");

file-loaderはmodule.exports.defaultに何も吐き出してないので、commonjsモードでimport foo from "./foo"と読み込むと、undefinedになってしまう。

es2015の場合の話

TypeScriptはimport / export構文をいじらず、そのまま吐き出す。

import mod from "./module"
const a = () => {
    mod()
}

これをwebpackが受け取り処理する。

webpackのloaderはbabel同様、__esModuleフラグを利用した後方互換維持の処理をしている。(これを ESM interopと呼んでるそうだ)
source

/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };

最終的にはこんな感じになっている。

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo_txt__ = __webpack_require__(0);
/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__foo_txt___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__foo_txt__);


console.log("===========");
console.log("import * as txt1");
console.log("=>", __WEBPACK_IMPORTED_MODULE_0__foo_txt__);
console.log("===========");
console.log("===========");
console.log("import txt2");
console.log("=>", __WEBPACK_IMPORTED_MODULE_0__foo_txt___default.a); // .defaultへのアクセスではなく、Object.definePropertyのgetterとしてアクセスするように変換されている
console.log("===========");

これによって、結果的に`import foo from "foo.css"でも読み出せるようになってる模様。

この後方互換は今後どうするかは議論対象になっている様なので、ある程度注意はしたほうが良さそう(とはいえ、議論を軽く追ってみるとwebpack2の間に突然消える、のようなことが起きることはなさそうに思える)

Webpack2(babel)の仕様に併せてimportが壊れるケースもあるらしいので注意

まとめ

  • webpackのtree shakingな読み方はbabelと同じ事やってるので、es2015にすればloader系がいきなりぶっ壊れたりはしない。
  • 可能なら、型によって読み出しを制御しつつ、TypeScriptの手法に併せていった方が無難と思われる。
  • 「どうせloader使ってるならwebpackに併せてしまえばいいのでは?」という判断とかもありえるが、そこらへんは自己判断で。
  • commonjsにして変な読み出しをさせないようにする、というのも無くはなさそうだが、型で防ぐでも十分には思える
  • webpackもそのうちこの辺の読み出しに動きがあると思われるので、何処かでは書き換えが発生するものと考えたほうが良いだろう。
27
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
21