原文地址:https://marxjiao.com/2018/04/10/node-webpack/
正在學習node.js,這里介紹使用webpack來搭建基于TypeScript的node開發環境。
整個環境的必備功能
一套好的開發環境能讓開發者專注于代碼,而不必關系其它事情。這里先列出一些必要的條件。
- 一個命令就能啟動項目。
- 一個命令能打包項目。
- 開發時代碼改動能夠自動更新,最好是熱更新,而不是重啟服務,這里為后面和前端代碼一起調試做準備。
- 開發中能使用編輯器或者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 shaking
、HMR
都靠它。
{
"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
。