使用webpack搭建基於typescript的node開發環境
正在學習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的更多資訊可檢視 ofollow,noindex">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
。
參考資料
- Hot reload all the things!
- How to HMR on server side?
- ping/" target="_blank" rel="nofollow,noindex">Node.js Web應用程式碼熱更新的另類思路
- Don’t use nodemon, there are better ways!
- Webpack 做 Node.js 程式碼熱替換, 第一步