使用webpack搭建基于TypeScript的node开发环境

原文地址:https://marxjiao.com/2018/04/10/node-webpack/

正在学习node.js,这里介绍使用webpack来搭建基于TypeScript的node开发环境。

整个环境的必备功能

一套好的开发环境能让开发者专注于代码,而不必关系其它事情。这里先列出一些必要的条件。

  1. 一个命令就能启动项目。
  2. 一个命令能打包项目。
  3. 开发时代码改动能够自动更新,最好是热更新,而不是重启服务,这里为后面和前端代码一起调试做准备。
  4. 开发中能使用编辑器或者chrome调试,我本人习惯使用vscode。

基本搭建思路

全局使用ts,包括脚本,webpack配置文件。使用npm调用ts脚本,脚本使用ts-node执行,使用ts脚本调用webpack的api来打包编译文件。

npm scipts -> start-dev.ts -> webpack(webpackConfig)

这里解释下为什么使用ts脚本来调用webpack而不是直接将webpack命令写在npm scripts里。我的想法是All In Typescrpt,尽量做到能用ts的就不用js,使用webpack的node api能轻松实现用ts写webpack配置。这样把做还有一个好处就是可以把webpack的配置写成动态的,根据传入参数来生成需要的配置。

选型

到这里项目的选型已经很明了了。

  • TypeScript 项目使用的主语言,为前端开发添加强类型支持,能在编码过程中避免很多问题。
  • Koa 应用比较广泛。没有附加多余的功能,中间件即插即用。
  • Webpack 打包工具,开发中热加载。
  • ts-node 用来直接执行ts脚本。
  • start-server-webpack-plugin 很关键的webpack插件,能够在编译后直接启动服务,并且支持signal模式的热加载,配合webpack/hot/signal很好用。

环境搭建

我们先用Koa写一个简单的web server,之后针对这个server来搭建环境。

项目代码

新建server/app.ts,这个文件主要用来创建一个koa app。

import * as Koa from 'koa';

const app = new Koa();

app.use(ctx => {
    ctx.body = 'Hello World';
});

export default app;

我们需要另一个文件来启动server,并且监听server/app.ts的改变,来热加载项目。

新建server/server.ts

import * as http from 'http';
import app from './app';

// app.callback() 会返回一个能够通过http.createServer创建server的函数,类似express和connect。
let currentApp = app.callback();
// 创建server
const server = http.createServer(currentApp);
server.listen(3000);

// 热加载
if (module.hot) {
    // 监听./app.ts
    module.hot.accept('./app.ts', () => {
        // 如果有改动,就使用新的app来处理请求
        server.removeListener('request', currentApp);
        currentApp = app.callback();
        server.on('request', currentApp);
    });
}

编译配置

在写webpack配置之前,我们先写下ts配置和babel配置。

TypeScript配置

这里写的是webpack编译代码用的配置,后面还会介绍ts-node跑脚本时使用的配置。我们新建config/tsconfig.json

{
    "compilerOptions": {
        // module配置很重要,千万不能配置成commonjs,热加载会失效
        "module": "es2015",
        "noImplicitAny": true,
        "sourceMap": true,
        "moduleResolution": "node",
        "isolatedModules": true,
        "target": "es5",
        "strictNullChecks": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "inlineSources": false,
        "lib": ["es2015"]
    },
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}

babel配置

.babelrc,"modules": false很重要,tree shakingHMR都靠它。

{
  "presets": [["env", {"modules": false}]]
}

webpack配置

一般情况下需要准备2套webpack配置,一套用来开发,一套用来发布。前面已经说过了使用webpack的api来打包为动态创建webpack配置提供了可能。所以这里我们写一个WebpackConfig类,创建实例时根据参数,生成不同环境的配置。

开发环境和发布环境的区别

首先两个环境的mode是是不同的,开发环境是development,发布环境是production。关于mode的更多信息可查看webpack文档

开发环境需要热加载和启动服务,entry里需要配置'webpack/hot/signal',使用webpack-node-externals将'webpack/hot/signal'打包到代码里,添加HotModuleReplacementPlugin,使用start-server-webpack-plugin启动服务和开启热加载。

webpack配置内容

现在我们来写下webpack配置。重点写在注释中了。

新建文件config/Webpack.config.ts

import * as path from 'path';
import * as StartServerPlugin from "start-server-webpack-plugin";
import * as webpack from 'webpack';
import * as nodeExternals from 'webpack-node-externals';
import {Configuration, ExternalsElement} from 'webpack';

class WebpackConfig implements Configuration {
    // node环境
    target: Configuration['target'] = "node";
    // 默认为发布环境
    mode: Configuration['mode'] = 'production';
    // 入口文件
    entry = [path.resolve(__dirname, '../server/server.ts')];
    output = {
        path: path.resolve(__dirname, '../dist'),
        filename: "server.js"
    };
    // 这里为开发环境留空
    externals: ExternalsElement[] = [];
    // loader们
    module = {
        rules: [
            {
                test: /\.tsx?$/,
                use: [
                    // tsc编译后,再用babel处理
                    {loader: 'babel-loader',},
                    {
                        loader: 'ts-loader',
                        options: {
                            // 加快编译速度
                            transpileOnly: true,
                            // 指定特定的ts编译配置,为了区分脚本的ts配置
                            configFile: path.resolve(__dirname, './tsconfig.json')
                        }
                    }
                ],
                exclude: /node_modules/
            },
            {
                test: /\.jsx?$/,
                use: 'babel-loader',
                exclude: /node_modules/
            }
        ]
    };
    resolve = {
        extensions: [".ts", ".js", ".json"],
    };
    // 开发环境也使用NoEmitOnErrorsPlugin
    plugins = [new webpack.NoEmitOnErrorsPlugin()];
    constructor(mode: Configuration['mode']) {
        // 配置mode,production情况下用上边的默认配置就ok了。
        this.mode = mode;
        if (mode === 'development') {
            // 添加webpack/hot/signal,用来热更新
            this.entry.push('webpack/hot/signal');
            this.externals.push(
                // 添加webpack/hot/signal,用来热更新
                nodeExternals({
                    whitelist: ['webpack/hot/signal']
                })
            );
            const devPlugins = [
                // 用来热更新
                new webpack.HotModuleReplacementPlugin(),
                // 启动服务
                new StartServerPlugin({
                    // 启动的文件
                    name: 'server.js',
                    // 开启signal模式的热加载
                    signal: true,
                    // 为调试留接口
                    nodeArgs: ['--inspect']
                }),
            ]
            this.plugins.push(...devPlugins);
        }
    }
}

export default WebpackConfig;

编译脚本

使用ts-node来启动脚本时需要使用新tsconfig.json,这个编译目标是在node中运行。

在项目根目录新建tsconfig.json:

{
    "compilerOptions": {
        // 为了node环境能直接运行
        "module": "commonjs",
        "noImplicitAny": true,
        "sourceMap": true,
        "moduleResolution": "node",
        "isolatedModules": true,
        "target": "es5",
        "strictNullChecks": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "inlineSources": false,
        "lib": ["es2015"]
    },
    "exclude": [
        "node_modules",
        "**/*.spec.ts"
    ]
}

开发脚本

启动开发脚本,scripts/start-dev.ts:

import * as webpack from 'webpack';

import WebpackConfig from '../config/Webpack.config';

// 创建编译时配置
const devConfig = new WebpackConfig('development');
// 通过watch来实时编译
webpack(devConfig).watch({
    aggregateTimeout: 300
}, (err: Error) => {
    console.log(err);
});

package.json中添加

"scripts": {
    "dev": "rm -rf ./dist && ts-node ./scripts/start-dev.ts"
},

执行yarn dev,我们能看到项目启动了:
命令行输出:

命令行输出

浏览器展示:


浏览器展示

修改server/app.ts


import * as Koa from 'koa';

const app = new Koa();

app.use(ctx => {
-   ctx.body = 'Hello World';
+   ctx.body = 'Hello Marx';
});

export default app;

能看到命令行输出:


更新代码后命令行输出

刷新浏览器:


浏览器展示

可以看到热更新已经生效了。

发布打包脚本

新建打包脚本scripts/build.ts

import * as webpack from 'webpack';

import WebpackConfig from '../config/Webpack.config';

const buildConfig = new WebpackConfig('production');

webpack(buildConfig).run((err: Error) => {
    console.log(err);
});

package.json添加build命令:

"scripts": {
+   "build": "rm -rf ./dist && ts-node ./scripts/build.ts",
    "dev": "rm -rf ./dist && ts-node ./scripts/start-dev.ts"
},

执行yarn build就能看到dist/server.js。这个就是我们项目的产出。其中包含了node_modules中的依赖,这样做是否合理,还在探索中,欢迎讨论。

到此整个环境搭建过程就完成了。

完整项目代码MarxJiao/webpack-node

总结

这个项目重点在于热加载和All In TypeScript。

1. 为什么后端代码要热加载?

为了方便使用webpack中间件打包前端代码,这样不用重启后端服务就不用重新编译前端代码,重新编译是很耗时的。后续使用时,流程大概是这样的

start-dev.ts -> server端的webpack -> server代码 -> webpack中间件 -> 前端代码

这样能保证开发时只需要一个入口来启动,前后端都能热加载。

2. 实现热加载的关键点

  • webpack配置mode: 'development',为了NamedModulesPlugin插件
  • webpack配置entry: 'webpack/hot/signal'
  • 将'webpack/hot/signal'打包进代码:nodeExternals({whitelist: ['webpack/hot/signal']})
  • 使用HotModuleReplacementPlugin
  • start-server-webpack-plugin配置signal: true
  • babel配置"modules": false
  • tsconfig.json配置"module": "es2015"
  • 使用单独的文件来启动server,监听热加载的文件,server/server.ts

3. tsconfig

ts-node运行脚本的tsconfig和ts-loader打包代码时的tsconfig不同。

ts-node用的config直接将代码用tsc编译后在node运行,在node 8.x以下的版本中不能使用import,所以module要用commonjs

webpack打包的代码要热加载,需要用es module,这里我们使用es2015

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,093评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,870评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,133评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,638评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,652评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,173评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,560评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,512评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,048评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,088评论 3 344
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,232评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,856评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,548评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,020评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,152评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,729评论 3 380
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,255评论 2 363

推荐阅读更多精彩内容

  • 一、webpack的基本概念 webpack 本质上是一个打包工具,它会根据代码的内容解析模块依赖,帮助我们把多个...
    cilla123阅读 1,547评论 0 8
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,699评论 7 110
  • 大三的寒假已一月有余,三十多天几乎是一直在虚度。时间慢慢流逝,原本制定了一堆计划也无从实施。没有目标、计划...
    祎言阅读 309评论 0 1
  • 那时候,时光变的很慢很慢!我静静地在塔顶看着那轮红日,愈来愈低,直到低下了山头,才不舍地离去……
    花晞阅读 104评论 0 1
  • 时间 尽量少做“短半衰期”的事情长半衰期事件指南 积累可信的知识 训练实践技能 构建新的思维模式 提升审美品位 反...
    Lv1_Sans阅读 361评论 0 0