用typescript 写一个webpack多页应用架构

虽然网上有很多webpack多页面相关的内容,但是大部分都很老旧了,用typescript写webpack配置的很基本没有。这里主要是给喜欢typescript的同学提供一点参考。

下面就是正文了

目录结构

├── build // webpack 配置目录
│   ├── helper // webpack配置中的一些工具函数
│   │   ├── constants.ts
│   │   ├── entries.helper.ts
│   │   └── template-compiler.helper.ts
│   ├── loader
│   │   ├── include-html-loader.ts
│   │   └── schema.ts
│   ├── webpack.base.conf.ts
│   ├── webpack.dev.conf.ts
│   └── webpack.prod.conf.ts
├── src
│   ├── assets
│   │   ├── images
│   │   │   └── 1.png
│   │   ├── script
│   │   │   ├── a.ts
│   │   │   └── b.ts
│   │   └── style
│   │       └── common.scss
│   ├── components
│   │   ├── footer.html
│   │   └── header.html
│   └── pages
│       ├── index.html
│       ├── index.scss
│       ├── index.ts
│       ├── page-a
│       │   ├── index.html
│       │   ├── index.scss
│       │   ├── index.ts
│       │   └── sub-page-a
│       │       ├── index.html
│       │       ├── index.ts
│       │       └── sub-sub-page-a
│       │           ├── index.html
│       │           └── index.ts
│       ├── page-b
│       │   ├── index.html
│       │   └── index.ts
│       └── page-c
│           ├── index.html
│           └── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json

开始

采用的是现在比较流行的分环境,分文件的方式

webpack 配置

// webpack.base.conf.ts
import * as webpack from 'webpack';
import {getEntries} from './helper/entries.helper';
import * as HtmlWebpackPlugin from 'html-webpack-plugin';
import {templateCompilerConfig} from './helper/template-compiler.helper';
import {ASSETS_DIR} from './helper/constants';
import * as ManifestPlugin from 'webpack-manifest-plugin';
import {resolve} from 'path';

const config: webpack.Configuration = {
  entry: getEntries(), // 配置多入口的helper函数, 后面会详细讲解
  resolve: {
    alias: {
      src: resolve(__dirname, '../src/')
    },
    // webpack编译时匹配的文件后缀,很关键
    // 有些同学不能识别ts文件就是因为没有配置该项
    extensions: ['.ts', '.js'] 
  },
  // 常规的用loader来处理各种文件
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /(node_modules|build)/,
        include: /src/,
        use: [
          {
            loader: 'ts-loader'
          }
        ]
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader', 'postcss-loader']
      },
      {
        test: /\.scss$/,
        use: [
          'style-loader',
          'css-loader',
          'postcss-loader',
          {
            loader: 'sass-loader',
            // sass-loader编译用的是dart-sass的实现
            options: {implementation: require('sass')}
          },
          {
            loader: 'sass-resources-loader',
            options: {
              resources: ['./src/assets/style/common.scss']
            }
          }
        ]
      },
      {
        test: /\.html$/,
        use: [
          'html-loader',
          // {
          //   loader: resolve(__dirname, './loader/include-html-loader.ts')
          // }
          // 这个地方是我自己写的一个loader,开始是集成在项目中,后来提取成一个npm包了
          'include-template-loader'
        ]
      },
      {
      // 处理图片,小于5k的直接base64编译到html中
        test: /\.(png|jpg|gif|jpeg|svg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 5 * 1024,
              name: `${ASSETS_DIR}/img/[name].[hash:6].[ext]`
            }
          }
        ]
      },
      {
      // 处理字体文件,和上面一样的方案
        test: /\.(woff2?|eot|ttf|otf)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 5 * 1024,
              name: `${ASSETS_DIR}/font/[name].[hash:6].[ext]`
            }
          }
        ]
      }
    ]
  },
  plugins: [
    // 多入口的html-webpack-plugin处理,后面详细讲解
    ...templateCompilerConfig().map(config => new HtmlWebpackPlugin(config)),
    new ManifestPlugin(),
    // 注册全局的依赖
    new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    })
  ],
  // 优化,提取公共的包
  optimization: {
    minimize: true,
    splitChunks: {
      cacheGroups: {
        vendor: {
          chunks: 'initial',
          test: /node_modules/,
          name: 'vendor',
          minSize: 0,
          priority: 10
        },
        common: {
          chunks: 'initial',
          name: 'common',
          minSize: 0,
          minChunks: 2 // 两个文件引用了就生成一个common包
        }
      }
    }
  }
};

export default config;

// webpack.prod.conf.ts
import * as webpack from 'webpack';
import {resolve} from 'path';
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin';
import {CleanWebpackPlugin} from 'clean-webpack-plugin';
import * as merge from 'webpack-merge';
import config from './webpack.base.conf';
import {ASSETS_DIR, PUBLIC_PATH} from './helper/constants';

const prodConfig: webpack.Configuration = {
  mode: 'production',
  output: {
    path: resolve(__dirname, '../dist'),
    filename: `${ASSETS_DIR}/js/[name].[hash:6].bundle.js`,
    publicPath: PUBLIC_PATH
  },
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader, // 生产环境提取css到文件
          'css-loader',
          'postcss-loader',
          {
            loader: 'sass-loader',
            options: {implementation: require('sass')}
          },
          {
            loader: 'sass-resources-loader',
            options: {
              resources: ['./src/assets/style/common.scss']
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: `${ASSETS_DIR}/css/[name].[hash:6].css`,
      chunkFilename: `${ASSETS_DIR}/css/[id].[hash:6].css`
    })
  ]
};

export default merge(config, prodConfig);

// webpack.dev.conf.ts
import * as webpack from 'webpack';
import * as merge from 'webpack-merge';
import config from './webpack.base.conf';
import {resolve} from 'path';
import {ASSETS_DIR, PUBLIC_PATH} from './helper/constants';

const devConfig: webpack.Configuration = {
  output: {
    path: resolve(__dirname, '../dist'),
    filename: `${ASSETS_DIR}/js/[name].bundle.js`, // 开发环境没有加hash
    publicPath: PUBLIC_PATH
  },
  mode: 'development',
  devtool: 'eval-source-map',
  // 用webpack-dev-server 实现开发服务器
  devServer: {
    port: 4000
    // writeToDisk: true
  }
};

export default merge(config, devConfig);

上面都是一些常规的配置,下面来介绍多页面的入口的处理

处理多页面的入口

// helper/entries.helper.ts
import {readdirSync, statSync} from 'fs';
import {join, sep} from 'path';
import {SOURCE_PATH} from './constants';

// 获取所有的入口文件的路径,用递归的方式处理
export const getEntryFiles = () => {
  const getFiles = (baseUrl: string, files: string[] = []) => {
    // 读取文件夹
    readdirSync(baseUrl)
      // 处理成绝对路径
      .map(item => join(baseUrl, item))
      // 遍历路径
      .forEach(item => {
        // 如果是文件夹,递归调用
        if (statSync(item).isDirectory()) {
          getFiles(item, files);
          // 如果是index.js || index.ts 就是我们要找的入口文件路径,放入数组中
        } else if (/index\.[tj]s/.test(item)) {
          files.push(item);
        }
      });
    return files;
  };
  // 返回所有合法的入口路径
  return getFiles(SOURCE_PATH);
};

// 构建入口对象
export const getEntries = () => {
  // 存放最终入口的对象
  const entries: {[key: string]: string} = {};
  getEntryFiles().forEach(path => {
    const entryName = getEntryName(path);
    entries[entryName] = path;
  });
  return entries;
};
// 获取入口的名称,该方法主要是为了编译后还能保持现有的目录结构
// 很符合直觉的在html中通过相对路径的方式跳转到其他页面,网上很多方案都不是很符合直觉
export const getEntryName = (path: string) => {
  const basename = path.replace(SOURCE_PATH, '').replace(sep, '');
  // 在windows中目录分割符是\, 统一替换成 /
  return basename.substring(0, basename.lastIndexOf('.')).replace(/\\/g, '/');
};

处理html-webpack-plugin


import {getEntryFiles, getEntryName} from './entries.helper';
import * as HtmlWebpackPlugin from 'html-webpack-plugin';

// 返回入口文件对应的html模版
export const templateCompilerConfig: () => HtmlWebpackPlugin.Options[] = () => {
  return getEntryFiles().map(path => {
    // 通过正则匹配的方式,替换ts||js后缀为html
    const template = path.replace(/[^\.]*(?!.*\.)/, 'html');
    // 符合直觉的相对路径跳转html的关键
    const name = getEntryName(template);
    return {
      template: template,
      inject: true,
      filename: `${name}.html`,
      chunks: ['vender', 'common', name]
    };
  });
};

以上就是所有的webpack配置,没有用到Babel,也没有polyfill,如果还需要其他功能可以自行发挥。
本项目主要解决了多页面应用a标签跳转的问题,不需要在写html模版的时候用到一些特殊的写法,非常符合直觉。
最后在添加npm script
"dev": "webpack-dev-server --config ./build/webpack.dev.conf.ts",
"build": "webapck --config ./build/webpack.prod.conf.ts"

webpack 本身就有对ts配置文件的支持,只需要安装ts-node就行了


版权声明:本文为huzzzz原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。