终南之巅 發表於 2022-7-4 00:35:00

typescript+webpack+api-extractor构建一个js库

<ul>
<li>依赖说明</li>
<li>入口文件</li>
<li>tsconfig配置</li>
<li>webpack配置文件
<ul>
<li>webpack入口文件配置</li>
<li>webpack为typescript和less文件配置各自的loader</li>
<li>webpack的output配置</li>
<li>运行webpack进行打包</li>
<li>测试验证</li>
</ul>
</li>
<li>输出esm模块
<ul>
<li>已经输出了umd格式的js了, 为什么还要输出esm模块? ----TreeShaking</li>
<li>用tsc输出esm和类型声明文件</li>
</ul>
</li>
<li>完善package.json文件
<ul>
<li>package.json中添加<code>exports</code>配置声明模块导出路径</li>
</ul>
</li>
<li>用api-extractor提取出干净的<code>.d.ts</code>
<ul>
<li>配置使用API extractor</li>
<li>更新<code>package.json</code></li>
<li>用@internal标注只希望在内部使用的class</li>
</ul>
</li>
<li>小结</li>
</ul>
<p>记录使用typescript配合webpack打包一个javascript library的配置过程.</p>
<p>目标是构建一个可以同时支持<code>CommonJs</code>, <code>esm</code>, <code>amd</code>这个几个js模块系统的javascript库, 然后还有一个单独打包出一个css的样式文件的需求.</p>
<p>为此以构建一个名为<code>loaf</code>的javascript库为例; 首先新建项目文件目录<code>loaf</code>, 并进入此文件目录执行<code>npm init</code>命令, 然后按照控制台的提示输入对应的信息, 完成后就会在loaf目录下得到一个<code>package.json</code>文件</p>
<p><img src="https://minio.mytechsky.top/blog/images/2022070319200387-20220703192002.png" alt="image" loading="lazy"></p>
<p>然后使用<code>npm i</code>命令安装所需的依赖</p>
<pre><code class="language-shell">npm i -D webpack webpack-cli typescript babel-loader @babel/core @babel/preset-env @babel/preset-typescript ts-node @types/node @types/webpack mini-css-extract-plugin css-minimizer-webpack-plugin less less-loader terser-webpack-plugin
</code></pre>
<h2 id="依赖说明">依赖说明</h2>
<ul>
<li><code>webpack webpack-cli</code>: webpack打包工具和webpack命令行接口</li>
<li><code>typescript</code>: 用于支持typescript语言</li>
<li><code>babel-loader @babel/core @babel/preset-env @babel/preset-typescript</code>: babel相关的东西, 主要是需要<code>babel-loader</code>将编写的typescript代码转译成es5或es6已获得更好的浏览器兼容性</li>
<li><code>ts-node @types/node @types/webpack</code>: 安装这几个包是为了能用typescript编写webpack配置文件(<code>webpack.config.ts</code>)</li>
<li><code>mini-css-extract-plugin less less-loader</code>: 编译提取less文件到单独的css文件的相关依赖</li>
<li><code>css-minimizer-webpack-plugin terser-webpack-plugin</code>: 用于最小化js和css文件尺寸的webpack插件</li>
</ul>
<h2 id="入口文件">入口文件</h2>
<p>通常使用<code>index.ts</code>作为入口, 并将其放到<code>src</code>目录下, 由于有输出样式文件的需求, 所以还要新建<code>styles/index.less</code></p>
<pre><code class="language-shell">mkdir src &amp;&amp; touch src/index.ts
mkdir src/styles &amp;&amp; touch src/styles/index.less
</code></pre>
<h2 id="tsconfig配置">tsconfig配置</h2>
<p>新建<code>tsconfig.json</code>文件</p>
<pre><code class="language-shell">touch tsconfig.json
</code></pre>
<p>填入以下配置(部分选项配有注释):</p>
<pre><code class="language-jsonc">{
    "compilerOptions": {
      "outDir": "dist/lib",
      "sourceMap": false,
      "noImplicitAny": true,
      "module": "commonjs",
      // 开启这个选项, 可以让你直接通过`import`的方式来引用commonjs模块
      // 这样你的代码库中就可以统一的使用import导入依赖了, 而不需要另外再使用require导入commonjs模块
      "esModuleInterop": true,
      // 是否允许合成默认导入
      // 开启后, 依赖的模块如果没有导出默认的模块
      // 那么typescript会帮你给该模块自动合成一个默认导出让你可以通过默认导入的方式引用该模块
      "allowSyntheticDefaultImports": true,
      // 是否生成`.d.ts`的类型声明文件
      "declaration": true,
      // 输出的目标js版本, 这里用es6, 然后配和babel进行转译才以获得良好的浏览器兼容
      "target": "es6",
      "allowJs": true,
      "moduleResolution": "node",
      "lib": ["es2015", "dom"],
      "declarationMap": true,
      // 启用严格的null检查
      "strictNullChecks": true,
      // 启用严格的属性初始化检查
      // 启用后类属性必须显示标注为可空或赋一个非空的初始值
      "strictPropertyInitialization": true
    },
    "exclude": ["node_modules"],
    "include": ["src/**/*"]
}
</code></pre>
<h2 id="webpack配置文件">webpack配置文件</h2>
<p>创建<code>webpack.config.ts</code></p>
<pre><code class="language-shell">touch webpack.config.ts
</code></pre>
<details open="true">
<summary>webpack.config.ts</summary>
<pre><code class="language-typescript">import path from "path";
import { Configuration, Entry } from "webpack";
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import CssMinimizer from 'css-minimizer-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin'

const isProd = process.env.NODE_ENV === 'production';

/**
* 这里用到了webpack的(https://webpack.js.org/guides/entry-advanced/)特性
* 注意`.less`入口文件必须放在`.ts`文件前 */
const entryFiles: string[] = ['./src/styles/index.less', './src/index.ts'];
const entry: Entry = {
index: entryFiles,
'index.min': entryFiles,
};

const config: Configuration = {
entry,
optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({ test: /.min.js$/ }),
      new CssMinimizer({
      test: /.min.css$/,
      }),
    ],
},
module: {
    rules: [
      {
      test: /.ts$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
          presets: ['@babel/env', '@babel/typescript'],
      },
      },
      {
      test: /.less$/,
      use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          {
            loader: 'css-loader',
          },
          'postcss-loader',
          'less-loader',
      ],
      },
    ],
},
output: {
    path: path.resolve(__dirname, 'dist/umd'),
    library: {
      type: 'umd',
      name: {
      amd: 'loaf',
      commonjs: 'loaf',
      root: 'loaf',
      },
    },
},
resolve: {
    extensions: ['.ts', '.less'],
},
devtool: 'source-map',
plugins: [
    new MiniCssExtractPlugin({
      filename: '.css',
    }),
],
};


export default config;
</code></pre>
</details>
<h3 id="webpack入口文件配置">webpack入口文件配置</h3>
<pre class="language-typescript" data-lines-highlight=""><code class="language-typescript" data-lines-highlight="">...
const isProd = process.env.NODE_ENV === 'production';

/**
* 这里用到了webpack的(https://webpack.js.org/guides/entry-advanced/)特性
* 注意`.less`入口文件必须放在`.ts`文件前 */
const entryFiles: string[] = ['./src/styles/index.less', './src/index.ts'];
const entry: Entry = {
index: entryFiles,
'index.min': entryFiles,
};

const config: Configuration = {
entry,
...
}
...</code></pre><p>在上面的<code>webpack.config.json</code>中,我们配置了两个入口分别是<code>index</code>和<code>index.min</code>,不难看出,多出的一个<code>index.min</code>入口是为了经过压缩后js和css文件,在生产环境使用一般都会使用<code>.min.js</code>结尾的文件以减少网络传输时的尺寸; 实现这个还需要结合<code>optimization</code>相关配置, 如下:</p>
<pre><code class="language-typescript">optimization: {
   minimize: true,
   minimizer: [
   new TerserPlugin({ test: /.min.js$/ }),
   new CssMinimizer({
       test: /.min.css$/,
   }),
   ],
},
</code></pre>
<p>另外,<code>index</code>和<code>index.min</code>的值都是相同的<code>entryFiles</code>对象,这个对象是一个字符串数组,里面放的就是我们的入口文件相对路径,这里一定要注意把<code>./src/styles/index.less</code>置于<code>./src/index.ts</code>之前。</p>
<h3 id="webpack为typescript和less文件配置各自的loader">webpack为typescript和less文件配置各自的loader</h3>
<p>配置完入口后, 就需要为typescript和less代码配置各自的loader</p>
<pre><code class="language-typescript">module: {
    rules: [
      {
      test: /.ts$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      options: {
          presets: ['@babel/env', '@babel/typescript'],
      },
      },
      {
      test: /.less$/,
      use: [
          isProd ? MiniCssExtractPlugin.loader : 'style-loader',
          {
            loader: 'css-loader',
          },
          'postcss-loader',
          'less-loader',
      ],
      },
    ],
},
</code></pre>
<ul>
<li><code>mini-css-extract-plugin less less-loader</code>: 编译提取less文件到单独的css文件的相关依赖<br>
上面的配置为.ts结尾的文件配置了<code>babel-loader</code>; 为<code>.less</code>结尾的文件配置一串loader, 使用了<code>use</code>, use中的loader的执行顺序是从后往前的, 上面less的配置就是告诉webpack遇到less文件时, 一次用<code>less-loader</code>-&gt;<code>postcss-loader</code>-&gt;<code>css-loader</code>-&gt;<code>生产环境用 MiniCssExtractPlugin.loader() 否则用 style-loader</code>;</li>
</ul>
<p><code>MiniCssExtractPlugin.loader</code>使用前要先在<code>plugins</code>进行初始化</p>
<pre class="language-typescript" data-lines-highlight=""><code class="language-typescript" data-lines-highlight="">...
const config = {
...
plugins: [
    new MiniCssExtractPlugin({
      filename: '.css',
    }),
],
...
}
...</code></pre><h3 id="webpack的output配置">webpack的output配置</h3>
<pre><code class="language-typescript">...
const config = {
...
output: {
    path: path.resolve(__dirname, 'dist/umd'),
    library: {
      type: 'umd',
      name: {
      amd: 'loaf',
      commonjs: 'loaf',
      root: 'loaf',
      },
    },
},
...
}
...
</code></pre>
<p>这里配置webpack以umd的方式输出到相对目录<code>dist/umd</code>目录中, <code>umd</code>是<code>Universal Module Definition</code>(通用模块定义)的缩写, umd格式输出library允许用户通过<code>commonjs</code>, <code>AMD</code>, <code>&lt;script src="..."&gt;</code>的方式对library进行引用<code>config.library.name</code>可以为不同的模块系统配置不同的导出模块名供客户端来进行引用; 由于这里的导出模块名都是<code>loaf</code>, 所以也可以直接<code>config.library.name</code>设置成<code>loaf</code>.</p>
<h3 id="运行webpack进行打包">运行webpack进行打包</h3>
<p>现在回到最开始通过<code>npm init</code>生成的<code>package.json</code>文件, 在修改其内容如下</p>
<pre class="language-json" data-lines-highlight=""><code class="language-json" data-lines-highlight="">{
"name": "loaf",
"version": "1.0.0",
"description": "A demo shows how to create &amp; build a javascript library with webpack &amp; typescript",
"main": "index.js",
"scripts": {
    "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production",
    "test": "npm run test"
},
"keywords": [
    "demo"
],
"author": "laggage",
"license": "MIT",
"devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@types/node": "^18.0.0",
    "@types/webpack": "^5.28.0",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^4.0.0",
    "less": "^4.1.3",
    "less-loader": "^11.0.0",
    "mini-css-extract-plugin": "^2.6.1",
    "postcss-loader": "^7.0.0",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.3.3",
    "ts-node": "^10.8.2",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
}
}</code></pre><p>新增了一个脚本命令<code> "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production"</code>, 然后命令行到项目目录下执行<code>npm run build:umd</code>, 不出意外应该就构建成功了, 此时生成的dist目录结构如下</p>
<pre><code class="language-plaintext">dist
└── umd
    ├── index.css
    ├── index.js
    ├── index.js.map
    ├── index.min.css
    ├── index.min.js
    └── index.min.js.map

1 directory, 6 files
</code></pre>
<h3 id="测试验证">测试验证</h3>
<p>新建<code>demo.html</code>进行测试</p>
<pre><code class="language-shell">mkdir demo &amp;&amp; touch demo/demo.html
</code></pre>
<details open="">
<summary>demo/demo.html</summary>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;script src="../dist/umd/index.js"&gt;&lt;/script&gt;
    &lt;script type="text/javascript"&gt;
      console.log(loaf, '\n', loaf.Foo)
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
</details>
<p>用浏览器打开<code>demo.html</code>, 然后F12打开控制台, 可以看到如下输出则说明初步达成了目标:</p>
<pre><code class="language-plaintext">Module&nbsp;{__esModule: true, Symbol(Symbol.toStringTag): 'Module'}
demo.html:13 ƒ Foo() {
    var _bar = arguments.length &gt; 0 &amp;&amp; arguments !== undefined ? arguments : new Bar();

    src_classCallCheck(this, Foo);

    this._bar = _bar;
}
</code></pre>
<h2 id="输出esm模块">输出esm模块</h2>
<p>完成上面的步骤后, 我们已经到了一个umd模块的输出, 相关文件都在<code>dist/umd</code>目录下; 其中包含可供<code>CommonJs</code> <code>ESM</code> <code>AMD</code>模块系统和<code>script标签</code>使用的umd格式的javascript文件和一个单独的css样式文件.</p>
<h3 id="已经输出了umd格式的js了-为什么还要输出esm模块-----treeshaking">已经输出了umd格式的js了, 为什么还要输出esm模块? ----TreeShaking</h3>
<blockquote>
<p>Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export. The name and concept have been popularized by the ES2015 module bundler rollup.</p>
</blockquote>
<p>此库的使用者也使用了类似webpack之类的支持Tree Shaking<br>
的模块打包工具,需要让使用者的打包工具能对这个js库<code>loaf</code>进行死代码优化Tree Shaking</p>
<p>从webpack文档中看出, tree-shaking依赖于ES2015(<code>ES2015 module syntax</code>, ES2015=ES6)的模块系统, tree-shaking可以对打包体积有不错优化, 所以为了支持使用者进行<code>tree-shaking</code>, 输出esm模块(esm模块就是指 ES2015 module syntax)是很有必要的.</p>
<h3 id="用tsc输出esm和类型声明文件">用tsc输出esm和类型声明文件</h3>
<pre><code class="language-shell">tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm
</code></pre>
<p>上面的命令使用typescript编译器命令行接口<code>tsc</code>输出了ES6模块格式的javascript文件到<code>dist/lib-esm</code>目录下</p>
<p>将这个目录加入到<code>package.json</code>的<code>scripts</code>配置中:</p>
<details open="">
<summary>package.json</summary>
<pre class="language-json" data-lines-highlight=""><code class="language-json" data-lines-highlight="">{
"name": "loaf",
"version": "1.0.0",
"description": "A demo shows how to create &amp; build a javascript library with webpack &amp; typescript",
"main": "index.js",
"scripts": {
    "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production",
    "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm",
    "test": "npm run test"
},
"keywords": [
    "demo"
],
"author": "laggage",
"license": "MIT",
"devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@types/node": "^18.0.0",
    "@types/webpack": "^5.28.0",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^4.0.0",
    "less": "^4.1.3",
    "less-loader": "^11.0.0",
    "mini-css-extract-plugin": "^2.6.1",
    "postcss-loader": "^7.0.0",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.3.3",
    "ts-node": "^10.8.2",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
}
}</code></pre></details>
<p>然后运行: <code>npm run build:lib-esm</code>, 此时dist目录结构如下:</p>
<pre><code class="language-plaintext">dist
├── lib-esm
│   ├── bar.js
│   └── index.js
├── typings
│   ├── bar.d.ts
│   ├── bar.d.ts.map
│   ├── index.d.ts
│   └── index.d.ts.map
└── umd
    ├── index.css
    ├── index.js
    ├── index.js.map
    ├── index.min.css
    ├── index.min.js
    └── index.min.js.map

3 directories, 12 files
</code></pre>
<p>多出了两个子目录分别为<code>lib-esm</code>和<code>typings</code>, 分别放着es6模块格式的javascript输出文件和typescript类型声明文件.</p>
<h2 id="完善packagejson文件">完善package.json文件</h2>
<p>到目前为止, <code>package.json</code>的scripts配置中, 已经有了<code>build:umd</code>和<code>build:lib-esm</code>用于构建umd格式的输出和esm格式的输出, 现在我们再向添加一个<code>build</code>用来组合<code>build:umd</code>和<code>build:lib-esm</code>并进行最终的构建,再次之前先安装一个依赖<code>shx</code>, 用于跨平台执行一些shell脚本: <code>npm i -D shx</code>;</p>
<p>更新<code>package.json</code>文件:</p>
<details open="true">
<summary>package.json</summary>
<pre class="language-json" data-lines-highlight=""><code class="language-json" data-lines-highlight="">{
"name": "loaf",
"version": "1.0.0",
"description": "A demo shows how to create &amp; build a javascript library with webpack &amp; typescript",
"main": "index.js",
"scripts": {
    "build": "shx rm -rf dist/** &amp;&amp; npm run build:umd &amp;&amp; npm run build:lib-esm",
    "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production",
    "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm",
    "test": "npm run test"
},
"keywords": [
    "demo"
],
"author": "laggage",
"license": "MIT",
"devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@types/node": "^18.0.0",
    "@types/webpack": "^5.28.0",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^4.0.0",
    "less": "^4.1.3",
    "less-loader": "^11.0.0",
    "mini-css-extract-plugin": "^2.6.1",
    "postcss-loader": "^7.0.0",
    "shx": "^0.3.4",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.3.3",
    "ts-node": "^10.8.2",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
}
}</code></pre></details>
<p><code>package.json</code>文件生成typescript声明文件所在的路径(可以参考typescript官网:Including declarations in your npm package):</p>
<details open="true">
<summary>package.json</summary>
<pre class="language-json" data-lines-highlight=""><code class="language-json" data-lines-highlight="">{
"name": "loaf",
"version": "1.0.0",
"description": "A demo shows how to create &amp; build a javascript library with webpack &amp; typescript",
"main": "index.js",
"typings": "./typings",
"scripts": {
    "build": "shx rm -rf dist/** &amp;&amp; npm run build:umd &amp;&amp; npm run build:lib-esm",
    "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production",
    "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm",
    "test": "npm run test"
},
"keywords": [
    "demo"
],
"author": "laggage",
"license": "MIT",
"devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@types/node": "^18.0.0",
    "@types/webpack": "^5.28.0",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^4.0.0",
    "less": "^4.1.3",
    "less-loader": "^11.0.0",
    "mini-css-extract-plugin": "^2.6.1",
    "postcss-loader": "^7.0.0",
    "shx": "^0.3.4",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.3.3",
    "ts-node": "^10.8.2",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
}
}</code></pre></details>
<h4 id="packagejson中添加exports配置声明模块导出路径">package.json中添加<code>exports</code>配置声明模块导出路径</h4>
<p><code>package.json</code>中的<code>exports</code>字段用于告诉使用者引用此库时从哪里寻找对应的模块文件. 比如使用者可能通过esm模块引用此库:</p>
<pre><code class="language-es6">import { Foo } from 'loaf';

const foo = new Foo();
</code></pre>
<p>此时如果我们的package.json中没有指定<code>exports</code>字段, 那么模块系统会去寻找<code>node_modules/index.js</code>, 结果肯定是找不到的, 因为我们真正的esm格式的输出文件应该是在<code>node_modules/loaf/lib-esm</code>中的</p>
<p>于是我们可以这样来配置<code>exports</code>:</p>
<details open="">
<summary>package.json</summary>
<pre class="language-json" data-lines-highlight=""><code class="language-json" data-lines-highlight="">{
"name": "loaf",
"version": "1.0.0",
"description": "A demo shows how to create &amp; build a javascript library with webpack &amp; typescript",
"main": "index.js",
"typings": "./typings",
"exports": {
    "./*": "./lib-esm/*",
    "./umd/*": "./umd"
},
"scripts": {
    "build": "shx rm -rf dist/** &amp;&amp; npm run build:umd &amp;&amp; npm run build:lib-esm",
    "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production",
    "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings -m es6 --outDir dist/lib-esm",
    "test": "npm run test"
},
"keywords": [
    "demo"
],
"author": "laggage",
"license": "MIT",
"devDependencies": {
    "@babel/core": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-typescript": "^7.18.6",
    "@types/node": "^18.0.0",
    "@types/webpack": "^5.28.0",
    "babel-loader": "^8.2.5",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^4.0.0",
    "less": "^4.1.3",
    "less-loader": "^11.0.0",
    "mini-css-extract-plugin": "^2.6.1",
    "postcss-loader": "^7.0.0",
    "shx": "^0.3.4",
    "style-loader": "^3.3.1",
    "terser-webpack-plugin": "^5.3.3",
    "ts-node": "^10.8.2",
    "typescript": "^4.7.4",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0"
}
}</code></pre></details>
<h2 id="用api-extractor提取出干净的dts">用api-extractor提取出干净的<code>.d.ts</code></h2>
<p>在上面的用tsc输出esm和类型声明文件这一段中, 我们通过tsc命令输出了typescript了类型声明文件到<code>dist/types</code>目录下, 这个目录下有两个<code>.d.ts</code>文件, 分别是<code>bar.d.ts</code>和<code>foo.d.ts</code>, 通常是希望这些声明文件都在一个文件<code>index.d.ts</code>中的, 如果他们分散开了, 以本库为例, 如果我要使用本库中的<code>Bar</code>类, 那么我可能需要这样来导入:</p>
<pre><code class="language-typescript">import { Bar } from 'loaf/typings/bar';
</code></pre>
<p>我不觉得的这种导入方式是好的做法, 理想的导入方式应该像下面这样:</p>
<pre><code class="language-typescript">import { Bar } from 'loaf';
</code></pre>
<p>所以接下来, 还要引入微软提供的<code>api-extractor</code></p>
<h3 id="配置使用api-extractor">配置使用API extractor</h3>
<p>安装依赖:</p>
<pre><code class="language-shell">npm install -D @microsoft/api-extractor
</code></pre>
<p>再全局安装下:</p>
<pre><code class="language-shell">npm install -g @microsoft/api-extractor
</code></pre>
<p>生成<code>api-extractor.json</code></p>
<pre><code class="language-shell">api-extractor init
</code></pre>
<p>稍微修改下<code>api-extractor.json</code></p>
<p>&lt;</p><details><p></p>
<summary>api-extractor.json</summary>
<pre class="language-jsonc" data-lines-highlight=""><code class="language-jsonc" data-lines-highlight="">/**
* Config file for API Extractor.For more info, please visit: https://api-extractor.com
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
/**
   * Optionally specifies another JSON config file that this file extends from.This provides a way for
   * standard settings to be shared across multiple projects.
   *
   * If the path starts with "./" or "../", the path is resolved relative to the folder of the file that contains
   * the "extends" field.Otherwise, the first path segment is interpreted as an NPM package name, and will be
   * resolved using NodeJS require().
   *
   * SUPPORTED TOKENS: none
   * DEFAULT VALUE: ""
   */
// "extends": "./shared/api-extractor-base.json"
// "extends": "my-package/include/api-extractor-base.json"

/**
   * Determines the "&lt;projectFolder&gt;" token that can be used with other config file settings.The project folder
   * typically contains the tsconfig.json and package.json config files, but the path is user-defined.
   *
   * The path is resolved relative to the folder of the config file that contains the setting.
   *
   * The default value for "projectFolder" is the token "&lt;lookup&gt;", which means the folder is determined by traversing
   * parent folders, starting from the folder containing api-extractor.json, and stopping at the first folder
   * that contains a tsconfig.json file.If a tsconfig.json file cannot be found in this way, then an error
   * will be reported.
   *
   * SUPPORTED TOKENS: &lt;lookup&gt;
   * DEFAULT VALUE: "&lt;lookup&gt;"
   */
// "projectFolder": "..",

/**
   * (REQUIRED) Specifies the .d.ts file to be used as the starting point for analysis.API Extractor
   * analyzes the symbols exported by this module.
   *
   * The file extension must be ".d.ts" and not ".ts".
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   */
"mainEntryPointFilePath": "&lt;projectFolder&gt;/dist/typings-temp/index.d.ts",

/**
   * A list of NPM package names whose exports should be treated as part of this package.
   *
   * For example, suppose that Webpack is used to generate a distributed bundle for the project "library1",
   * and another NPM package "library2" is embedded in this bundle.Some types from library2 may become part
   * of the exported API for library1, but by default API Extractor would generate a .d.ts rollup that explicitly
   * imports library2.To avoid this, we can specify:
   *
   *   "bundledPackages": [ "library2" ],
   *
   * This would direct API Extractor to embed those types directly in the .d.ts rollup, as if they had been
   * local files for library1.
   */
"bundledPackages": [],

/**
   * Determines how the TypeScript compiler engine will be invoked by API Extractor.
   */
"compiler": {
    /**
   * Specifies the path to the tsconfig.json file to be used by API Extractor when analyzing the project.
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * Note: This setting will be ignored if "overrideTsconfig" is used.
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;projectFolder&gt;/tsconfig.json"
   */
    // "tsconfigFilePath": "&lt;projectFolder&gt;/tsconfig.json",
    /**
   * Provides a compiler configuration that will be used instead of reading the tsconfig.json file from disk.
   * The object must conform to the TypeScript tsconfig schema:
   *
   * http://json.schemastore.org/tsconfig
   *
   * If omitted, then the tsconfig.json file will be read from the "projectFolder".
   *
   * DEFAULT VALUE: no overrideTsconfig section
   */
    // "overrideTsconfig": {
    //   . . .
    // }
    /**
   * This option causes the compiler to be invoked with the --skipLibCheck option. This option is not recommended
   * and may cause API Extractor to produce incomplete or incorrect declarations, but it may be required when
   * dependencies contain declarations that are incompatible with the TypeScript engine that API Extractor uses
   * for its analysis.Where possible, the underlying issue should be fixed rather than relying on skipLibCheck.
   *
   * DEFAULT VALUE: false
   */
    // "skipLibCheck": true,
},

/**
   * Configures how the API report file (*.api.md) will be generated.
   */
"apiReport": {
    /**
   * (REQUIRED) Whether to generate an API report.
   */
    "enabled": true

    /**
   * The filename for the API report files.It will be combined with "reportFolder" or "reportTempFolder" to produce
   * a full file path.
   *
   * The file extension should be ".api.md", and the string should not contain a path separator such as "\" or "/".
   *
   * SUPPORTED TOKENS: &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;unscopedPackageName&gt;.api.md"
   */
    // "reportFileName": "&lt;unscopedPackageName&gt;.api.md",

    /**
   * Specifies the folder where the API report file is written.The file name portion is determined by
   * the "reportFileName" setting.
   *
   * The API report file is normally tracked by Git.Changes to it can be used to trigger a branch policy,
   * e.g. for an API review.
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;projectFolder&gt;/etc/"
   */
    // "reportFolder": "&lt;projectFolder&gt;/etc/",

    /**
   * Specifies the folder where the temporary report file is written.The file name portion is determined by
   * the "reportFileName" setting.
   *
   * After the temporary file is written to disk, it is compared with the file in the "reportFolder".
   * If they are different, a production build will fail.
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;projectFolder&gt;/temp/"
   */
    // "reportTempFolder": "&lt;projectFolder&gt;/temp/"
},

/**
   * Configures how the doc model file (*.api.json) will be generated.
   */
"docModel": {
    /**
   * (REQUIRED) Whether to generate a doc model file.
   */
    "enabled": true

    /**
   * The output path for the doc model file.The file extension should be ".api.json".
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;projectFolder&gt;/temp/&lt;unscopedPackageName&gt;.api.json"
   */
    // "apiJsonFilePath": "&lt;projectFolder&gt;/temp/&lt;unscopedPackageName&gt;.api.json"
},

/**
   * Configures how the .d.ts rollup file will be generated.
   */
"dtsRollup": {
    /**
   * (REQUIRED) Whether to generate the .d.ts rollup file.
   */
    "enabled": true,

    /**
   * Specifies the output path for a .d.ts rollup file to be generated without any trimming.
   * This file will include all declarations that are exported by the main entry point.
   *
   * If the path is an empty string, then this file will not be written.
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;projectFolder&gt;/dist/&lt;unscopedPackageName&gt;.d.ts"
   */
    "untrimmedFilePath": "&lt;projectFolder&gt;/dist/typing/index.d.ts"

    /**
   * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "beta" release.
   * This file will include only declarations that are marked as "@public" or "@beta".
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: ""
   */
    // "betaTrimmedFilePath": "&lt;projectFolder&gt;/dist/&lt;unscopedPackageName&gt;-beta.d.ts",

    /**
   * Specifies the output path for a .d.ts rollup file to be generated with trimming for a "public" release.
   * This file will include only declarations that are marked as "@public".
   *
   * If the path is an empty string, then this file will not be written.
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: ""
   */
    // "publicTrimmedFilePath": "&lt;projectFolder&gt;/dist/&lt;unscopedPackageName&gt;-public.d.ts",

    /**
   * When a declaration is trimmed, by default it will be replaced by a code comment such as
   * "Excluded from this release type: exampleMember".Set "omitTrimmingComments" to true to remove the
   * declaration completely.
   *
   * DEFAULT VALUE: false
   */
    // "omitTrimmingComments": true
},

/**
   * Configures how the tsdoc-metadata.json file will be generated.
   */
"tsdocMetadata": {
    /**
   * Whether to generate the tsdoc-metadata.json file.
   *
   * DEFAULT VALUE: true
   */
    // "enabled": true,
    /**
   * Specifies where the TSDoc metadata file should be written.
   *
   * The path is resolved relative to the folder of the config file that contains the setting; to change this,
   * prepend a folder token such as "&lt;projectFolder&gt;".
   *
   * The default value is "&lt;lookup&gt;", which causes the path to be automatically inferred from the "tsdocMetadata",
   * "typings" or "main" fields of the project's package.json.If none of these fields are set, the lookup
   * falls back to "tsdoc-metadata.json" in the package folder.
   *
   * SUPPORTED TOKENS: &lt;projectFolder&gt;, &lt;packageName&gt;, &lt;unscopedPackageName&gt;
   * DEFAULT VALUE: "&lt;lookup&gt;"
   */
    // "tsdocMetadataFilePath": "&lt;projectFolder&gt;/dist/tsdoc-metadata.json"
},

/**
   * Specifies what type of newlines API Extractor should use when writing output files.By default, the output files
   * will be written with Windows-style newlines.To use POSIX-style newlines, specify "lf" instead.
   * To use the OS's default newline kind, specify "os".
   *
   * DEFAULT VALUE: "crlf"
   */
// "newlineKind": "crlf",

/**
   * Configures how API Extractor reports error and warning messages produced during analysis.
   *
   * There are three sources of messages:compiler messages, API Extractor messages, and TSDoc messages.
   */
"messages": {
    /**
   * Configures handling of diagnostic messages reported by the TypeScript compiler engine while analyzing
   * the input .d.ts files.
   *
   * TypeScript message identifiers start with "TS" followed by an integer.For example: "TS2551"
   *
   * DEFAULT VALUE:A single "default" entry with logLevel=warning.
   */
    "compilerMessageReporting": {
      /**
       * Configures the default routing for messages that don't match an explicit rule in this table.
       */
      "default": {
      /**
         * Specifies whether the message should be written to the the tool's output log.Note that
         * the "addToApiReportFile" property may supersede this option.
         *
         * Possible values: "error", "warning", "none"
         *
         * Errors cause the build to fail and return a nonzero exit code.Warnings cause a production build fail
         * and return a nonzero exit code.For a non-production build (e.g. when "api-extractor run" includes
         * the "--local" option), the warning is displayed but the build will not fail.
         *
         * DEFAULT VALUE: "warning"
         */
      "logLevel": "warning"

      /**
         * When addToApiReportFile is true:If API Extractor is configured to write an API report file (.api.md),
         * then the message will be written inside that file; otherwise, the message is instead logged according to
         * the "logLevel" option.
         *
         * DEFAULT VALUE: false
         */
      // "addToApiReportFile": false
      }

      // "TS2551": {
      //   "logLevel": "warning",
      //   "addToApiReportFile": true
      // },
      //
      // . . .
    },

    /**
   * Configures handling of messages reported by API Extractor during its analysis.
   *
   * API Extractor message identifiers start with "ae-".For example: "ae-extra-release-tag"
   *
   * DEFAULT VALUE: See api-extractor-defaults.json for the complete table of extractorMessageReporting mappings
   */
    "extractorMessageReporting": {
      "default": {
      "logLevel": "warning"
      // "addToApiReportFile": false
      }

      // "ae-extra-release-tag": {
      //   "logLevel": "warning",
      //   "addToApiReportFile": true
      // },
      //
      // . . .
    },

    /**
   * Configures handling of messages reported by the TSDoc parser when analyzing code comments.
   *
   * TSDoc message identifiers start with "tsdoc-".For example: "tsdoc-link-tag-unescaped-text"
   *
   * DEFAULT VALUE:A single "default" entry with logLevel=warning.
   */
    "tsdocMessageReporting": {
      "default": {
      "logLevel": "warning"
      // "addToApiReportFile": false
      }

      // "tsdoc-link-tag-unescaped-text": {
      //   "logLevel": "warning",
      //   "addToApiReportFile": true
      // },
      //
      // . . .
    }
}
}</code></pre></details>
<h3 id="更新packagejson">更新<code>package.json</code></h3>
<pre class="language-json" data-lines-highlight=""><code class="language-json" data-lines-highlight="">{
...
"scripts": {
    "build": "shx rm -rf dist/** &amp;&amp; npm run build:umd &amp;&amp; npm run build:lib-esm &amp;&amp; npm run build:extract-api",
    "build:umd": "webpack -c webpack.config.ts --node-env production --env NODE_ENV=production",
    "build:lib-esm": "tsc -p tsconfig.json --declarationDir ./dist/typings-temp -m es6 --outDir dist/lib-esm",
    "build:extract-api": "api-extractor run &amp;&amp; shx rm -rf dist/typings-temp",
    "build:extract-api-local": "shx mkdir -p ./etc &amp;&amp; npm run build:lib-esm &amp;&amp; api-extractor run -l",
    "test": "npm run test"
},
...
}</code></pre><p>注意, 这里处理新增了一个<code>build:extract-api</code>到scripts配置中, 还修改了<code>build:lib-esm</code>的配置, 将其输出的typescript类型声明文件放到了, typings-temp目录中, 最后这个目录是要删除; 还要注意, 每次提交代码到版本管理工具前, 要先运行<code>npm run build:extract-api-local</code>, 这个命令会生成<code>./etc/&lt;libraryName&gt;.api.md</code>文件, 这个文件是api-extractor生成的api文档, 应该要放到版本管理工具中去的, 以便可以看到每次提交代码时API的变化.</p>
<h3 id="用internal标注只希望在内部使用的class">用@internal标注只希望在内部使用的class</h3>
<p>比如, 我希望<code>Bar</code>类不能被此库的使用者使用, 我可以加上下面这段注释</p>
<pre><code class="language-typescript">/**
*
* @internal
*/
export class Bar {
bar() {}
}
</code></pre>
<p>然后来看看生成的<code>index.d.ts</code>文件:</p>
<pre><code class="language-typescript">/**
*
* @internal
*/
declare class Bar {
    bar(): void;
}

export declare class Foo {
    private _bar;
    constructor(_bar?: Bar);
    foo(): void;
    loaf(): void;
}

export { }
</code></pre>
<p>可以看出<code>index.d.ts</code>文件中虽然declare了<code>Bar</code>, 但是并未导出<code>Bar</code></p>
<blockquote>
<p>这个特性是由api-extractor提供的, 更多api-extractor的内容移步官方文档</p>
</blockquote>
<h2 id="小结">小结</h2>
<p>至此, 我们就可以构建一个可以通过诸如<code>AMD</code> <code>CommonJs</code> <code>esm</code>等js模块系统或是使用<code>script标签</code>的方式引用的js库了, 主要用到了<code>webpack</code> <code>typescript</code> <code>api-extractor</code>这些工具. 完整的示例代码可以访问github-laggage/loaf查看.</p>


</div>
<div id="MySignature" role="contentinfo">
    <p>作者:Laggage</p>
<p>出处:https://www.cnblogs.com/laggage/p/build-js-library-with-webpack-and-typescript-and-api-extractor.html</p>
<p>说明:转载请注明来源</p><br><br>
来源:https://www.cnblogs.com/laggage/p/build-js-library-with-webpack-and-typescript-and-api-extractor.html
頁: [1]
查看完整版本: typescript+webpack+api-extractor构建一个js库