用Webpack构建基础的Typescript项目

1,906 阅读5分钟

本文使用Webpack 4和Typescript 3构建一个简单项目,旨在梳理基础的项目结构,常规的配置选项以及开发打包的流程。更多复杂和高级的选项则可以在此基础上进行扩展。

搭建项目

  1. 创建项目
mkdir webpack-sample
cd webpack-sample
npm init -y
  1. 安装依赖
  • typescript
npm install --save-dev typescript
  • lodash
npm install --save lodash
npm install --save-dev @types/lodash
  • webpack
npm install --save-dev webpack webpack-cli
npm install --save-dev ts-loader
npm install --save-dev clean-webpack-plugin html-webpack-plugin webpack-bundle-analyzer
  1. 项目结构
webpack-sample
|-- src
|   |-- index.html      // template
|   |-- main.ts         // entry
|   |-- card.ts
|-- webpack.config.js
|-- tsconfig.json
|-- package.json
|-- package-lock.json

源码内容

  1. index.html
  • 基础模板,提供一个空的container
<!DOCTYPE html>
<html>
    <head>
        <title>Webpack Sample</title>
    </head>
    <body>
        <div id="container"></div>
    </body>
</html>
  1. card.ts
  • 定义一个Card的class,用于生成Card元素
// card.ts
export class Card {
    title: string;
    desc: string;

    constructor(title: string, desc: string) {
        this.title = title;
        this.desc = desc;
    }

    genElement() {
        const card = document.createElement('div');
        const title = document.createElement('h2');
        title.innerHTML = this.title;
        const desc = document.createElement('div');
        desc.innerHTML = this.desc;

        card.appendChild(title)
        card.appendChild(desc);
        return card;
    }
}
  1. main.ts
  • 引入lodash和Card类
  • 循环生成多个Card元素并添加到页面中
// main.ts
import lodash from 'lodash';
import { Card } from './card';

const container = document.getElementById('container');
const cardList = [{
    title: 'Title A',
    desc: 'test test test test'
}, {
    title: 'Title B',
    desc: 'longgggggggggggggggggggggggggggggggtest'
}]

lodash.each(cardList, cardInfo => {
    const card = new Card(cardInfo.title, cardInfo.desc);
    const cardEle = card.genElement();
    container.appendChild(cardEle);
})

配置信息

  1. tsconfig.json
  • esModuleInterop选项是为了支持lodash引入时使用ES Module标准
// tsconfig.json
{
    "compilerOptions": {
        "target": "ES5",
        "esModuleInterop": true
    }
}
  1. webpack.config.js
  • resolve配置了webpack寻找module的方式
  • 针对ts文件,用ts-loader来加载
  • clean-webpack-plugin清理输出路径
  • html-webpack-plguin用于处理html模板
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: {
        main: './src/main.ts'
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].bundle.js',
    },
    module: {
        rules: [
            {
                test: /\.ts/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({template: './src/index.html'})
    ]
}
  1. 脚本
// package.json
{
    "scripts": {
        "build": "webpack"
    }
}

运行npm run build进行编译,在dist目录生成输出文件。双击index.html可以在浏览器中看到生成的页面。

样式表

  1. 为上述示例添加样式表
  • 添加src/card.scss文件,编辑内容
// card.scss
.card {
    padding: 4px;
    margin: 4px;
    border: 1px solid #333333;

    .desc {
        word-break: break-all;
    }
}
  • 修改src/card.ts文件,为生成的元素添加一些属性
// card.ts
genElement() {
    const card = document.createElement('div');
    const title = document.createElement('h2');
    title.innerHTML = this.title;
    const desc = document.createElement('div');
    desc.className = 'desc';  // 添加desc类名
    desc.innerHTML = this.desc;

    card.appendChild(title)
    card.appendChild(desc);
    card.className = 'card';  // 添加card类名
    return card;
}
  1. 在webpack打包流程中加入样式表处理
  • 安装依赖
npm install --save-dev style-loader css-loader sass-loader node-sass
  • 修改配置
// webpack.config.js
module.exports = {
    resolve: {
        extensions: ['.ts', '.js', '.scss']
    },
    rules: [
        {
            test: /\.s[ac]ss/,
            use: [
                'style-loader',
                'css-loader',
                'sass-loader'
            ]
        }
    ]
}
  • 引入模块
// main.ts
import './card.scss';
  1. 测试效果
  • 运行npm run build,然后在dist目录中打开index.html文件,可以看到样式已经应用到页面上
  • 可以看到dist目录中并没有css文件,因为style-loader默认使用的方式是styleTag,即用js将样式以style标签的形式写入到html中
  1. 使用linkTag添加样式
  • 上述配置方式是将样式直接添加到html中,对于样式表较多的情况不是很合适
  • 使用独立的样式表,以link标签添加到html中,方法如下
  • 安装插件mini-css-extract-plugin
npm install --save-dev mini-css-extract-plugin
  • 修改配置
// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.s[ac]ss/,
                use: [
                    // 'style-loader',
                    MiniCssExtractPlugin.loader,  // 使用MiniCssExtractPlugin.loader替换style-loader
                    'css-loader',
                    'sass-loader'
                ],
                exclude: /node_modules/
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({template: './src/index.html'}),
        new MiniCssExtractPlugin(),  // 该插件需要置于html插件之后
    ]
}
  • 再运行npm run build就会生成独立的css文件,并将linkTag添加到index.html中
  1. css后处理
  • 利用postcss-loader可以对css进行处理。例如利用autoprefixer为css添加浏览器前缀,配置如下
// package.json中配置目标浏览器
"browserslist": ["last 2 versions"]

// 添加postcss.config.js
module.exports = {
    plugins: [require('autoprefixer')]
}

// webpack.common.js
module: {
    rules: [
        {
            test: /\.s[ac]ss/,
            use: [
                // 'style-loader',
                MiniCssExtractPlugin.loader,
                'css-loader',
                'postcss-loader',  // 位置在css-loader之后
                'sass-loader'
            ],
            exclude: /node_modules/
        }
    ]
}

sourceMap

  1. 上述配置中没有开启sourceMap,对于开发调试不是很方便。例如,我们人为构造报错:
// card.ts
genElement() {
    if (this.desc.toLowerCase() === 'error') {
        throw Error('custom error');
    }
    ...
}

// main.ts
const cardList = [
    {
        title: 'Title Error',
        desc: 'error',
    }
];

若未开启sourceMap,则需要到bundle.js中进行查错,很不方便。

  1. 开启sourceMap需要修改tsconfig.json和webpack.config.js
// tsconfig.json
{
    "sourceMap": true
}
// webpack.config.js
module.exports = {
    devtool: 'cheap-module-eval-source-map'
}
  1. devtool还有更多选项,可以参考
  2. 如果配置mode为'development'(默认是'production', 不进行显式配置会提示warning),则相当于配置了devtool为'eval',例如
// webpack.config.js
module.exports = {
    mode: 'development',
    // devtool: 'eval'
}

webpack-dev-server

  • webpack-dev-server可以提供一个简单的web服务器对webpack打包后的文件伺服
  • 首先安装依赖
npm install --save-dev webpack-dev-server
  • 然后修改配置
// webpack.config.js
module.exports = {
    devServer: {
        contentBase: './dist'
    }
}
  • 增加脚本
// package.json
{
    "scripts": {
        "start": "webpack-dev-server --open"
    }
}

运行npm run start开始webpack打包流程并启动服务器,并打开网页查看。

  • 使用webpack-dev-server后默认不会在dist目录生成打包后的文件,而是将生成结果直接读入内存,然后在webpack-dev-server的根目录下进行伺服。

环境区分

  • 上述配置基本都是按照开发环境来进行的,针对生产环境需要使用不同的配置,例如不要source-map,压缩代码等
  • 可以按照文档指引来配置不同环境下的内容,主要流程即配置不通的config文件,然后在不同环境下指定使用不同配置文件
  • mode选项设置为developmentproduction时相当于配置了很多默认内容:参考文档
  • 生产环境的配置摘要
    • 静态文件名加上hash串
    // webpack.common.js
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].[contenthash].js'
    },
    plugins: [
        new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css'
        }),
    ]
    
    • js代码压缩:production模式下会自动开启minimize的优化选项,默认会使用terser-webpack-plugin进行js代码压缩
    • css代码压缩:虽然文档中说需要额外使用插件对css进行压缩,但是实际实验之后发现设定production模式后,css默认就已经压缩了

总结

至此,我们的项目已经构建完成,总结如下:

  • 搭建了基本的项目结构,包括模板,ts脚本,scss样式表
  • 配置了常规的构建打包的流程
  • 支持开发环境的调试需求,包括开发服务器,sourceMap等
  • 支持生产环境的打包需求

查看完整代码:github.com/moonlight82…