1. 程式人生 > 其它 >理解微前端 - 從部署一套自己的前端開發環境(腳手架)開始

理解微前端 - 從部署一套自己的前端開發環境(腳手架)開始

很久以前的前端,沒有太多工具化工程化思想,一堆程式碼塞進去完事兒。如今前端已經很卷,捲到了一個開發環境都夠你折騰一宿。那麼我們拋開類似nextjs、create-react-app這類的工具或框架,我們該如何從零部署一個屬於自己的開發環境呢?這篇文章將講述如何配置一個基礎的腳手架,支援React、TypeScrit和單元測試等必要的功能。在這個基礎上,也能夠很方便去個性化腳手架,讓它支援比如Electron、Mobx、Redux、PM2、Express(可用來實現SSR服務端渲染)等你想要的擴充套件功能。

首先,我們要知道對於實際的專案,當然是效率優先,儘可能使用成熟的工具和框架,比如類似Next.js、UmiJS、Ant Design等能夠簡化UI Elements和腳手架環境的東西,都推薦使用它。他們能夠一次性完成常用的UI架構,網站的SEO優化,服務端渲染,資源優化,效能優化(打包,懶載入等),安全優化等。對於一個專案的快速搭建和穩定性是有比較明顯的作用的。這裡我們學會從零部署開發環境,一方面可以利於提升自己擴充套件腳手架的能力,同時也能夠在未來的工具和框架運用中,更加靈活的加入自己的想法。

實質上,我們現在做的事情就是搭建一個比較基礎的“微前端架構(Micro-Frontend Architectures)”,至於更加豐富的功能,可以在這個基礎上不斷豐富。目前某些大廠,都開源了自己的微前端架構工具,相當於也是把整個Web架構集於一體,極大提高了前後端分離、開發協作、部署測試釋出迭代的整合效率,提高生產效率。蘿蔔各有所需,如果你的專案沒有較多的約束性,可以直接使用現成的微前端架構工具,比如Bit、Piral、Modern.js等。當然如果不學習如何搭建自己的腳手架,一旦脫離了別人幫你寫好的框架,你可能會顯得很懵~

微前端架構其實可以很複雜,複雜到一整個大團隊的協作,一個公司的業務體系,甚至不同語言開發者的分散式合作。也可以很簡潔,簡潔到讓他成為自己的生產工具,可以用來開發,除錯,可以用來部署釋出,可以用來協作交流。這次寫這篇文章,其實也是想拋開理論,去從某個角度去理解如何才算是自己的架構?它不一定非要打包釋出,不一定非要變成一個系統的框架,微前端架構,其實和自己的生存環境,工作方式息息相關。它並不是一種標準,相當於是人定義的一種規範,一個體系。用得好了,事半功倍,用不好了,還是有不好的影響的:)

如果要真正搭建一個微前端架構,是非常複雜的,涉及的知識面很廣,但是怎麼去理解它的運作,從一個很小的方面去體驗,就足夠了。真正要使用微前端架構,還是建議引入比較成熟的架構方案,相對於中小型產品,自己瞎折騰下還是可以的。

本文分為兩大部分,一個是基礎配置,一個是深入配置,它將能夠更好地適配你的React專案或者傳統的Web專案(當然我覺得Vue也是同理的思想)。讀完這篇文章,我們將實現一個由淺入深完成一套 Webpack5+TypeScript+Jest+ESLint+SASS+React 的開發環境,也可以把它當做腳手架成品。

必須:在這之前,我們要確保你已經安裝至少Node 10+以上的版本,我自己電腦目前的Node版本是v14.16.0

(假設你的專案名稱叫 my-react-app, 那麼 my-react-app 資料夾內就包含以下的目錄和檔案,你可以通過 cd /{your_directory}/my-react-app 命令進入你的目錄,使用 npm 命令安裝和移除依賴項,它也會同時修改 package.jsonpackage-lock.json 檔案,具體怎麼操作,相信你使用過Node的話,基本沒有啥問題的)

目錄如下:

下面的步驟,儘可能按照順序來:)

(一)建立package.json檔案

建立一個package.json檔案,滿足Webpack、TypeScript,Jest,ESLint等基本需求,同時它也滿足基本的JS應用的需求。
Node 專案在專案根目錄中名為 package.json 的檔案中跟蹤依賴關係和元資料。這是專案的核心。它包含名稱、描述和版本之類的資訊,以及執行、開發以及有選擇地將專案釋出到 npm 所需的資訊。如果大家想要詳細瞭解這個檔案的用途,可以參看NPM的官方文件 https://docs.npmjs.com/creating-a-package-json-file

關於某些Dependencies,具體的功能根據自己的需求增加,目前我主要針對我們要配置的這個環境選擇,多餘的依賴就不參與。

下面是已經建立好的示例程式碼(並非最基礎的package.json程式碼),你可以根據需要修改它們。

{
  "name": "my-react-app",
  "version": "0.0.1",
  "description": "My React App.",
  "main": "dist/my-react-app.js",
  "directories": {
    "test": "test"
  },
  "jest": {
    "testEnvironment": "jsdom",
    "transform": {
      "^.+\\.(js|jsx)$": "babel-jest",
      "^.+\\.(ts|tsx)?$": "ts-jest"
    }
  },
  "scripts": {
    "check": "tsc",
    "dev": "cross-env NODE_ENV=development webpack --progress --mode development --config build/config.js",
    "build": "cross-env NODE_ENV=production webpack --progress --mode production --config build/config.js",
    "test": "cross-env NODE_ENV=test jest"
  },
  "repository": {
    "type": "git",
    "url": "my-react-app"
  },
  "keywords": [
    "library"
  ],
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/xizon/my-react-app/issues"
  },
  "homepage": "https://github.com/xizon/my-react-app#readme",
  "devDependencies": {
    "@babel/core": "^7.13.14",
    "@babel/plugin-transform-runtime": "^7.16.4",
    "@babel/polyfill": "^7.0.0",
    "@babel/preset-env": "^7.2.0",
    "@babel/preset-typescript": "^7.1.0",
    "@types/jest": "^27.0.3",
    "@typescript-eslint/eslint-plugin": "^4.28.5",
    "@typescript-eslint/parser": "^4.28.5",
    "babel-loader": "^8.0.4",
    "babel-plugin-module-resolver": "^4.1.0",
    "cross-env": "^7.0.3",
    "eslint": "^7.32.0",
    "jest": "^27.0.4",
    "jsdom": "^18.1.1",
    "moment": "^2.29.1",
    "terser-webpack-plugin": "^5.1.4",
    "ts-jest": "^27.0.4",
    "ts-node": "^10.1.0",
    "typescript": "^4.3.5",
    "webpack": "^5.47.1",
    "webpack-cli": "^4.9.1"
  },
  "eslintConfig": {
    "parserOptions": {
      "parser": "@typescript-eslint/parser",
      "ecmaVersion": 2018,
      "sourceType": "module"
    },
    "extends": [
      "plugin:@typescript-eslint/recommended"
    ]
  },
  "dependencies": {},
  "author": "XXXXXXXXX"
}

(二)建立tsconfig.json檔案

tsconfig.json 檔案用來配置TypeScript,具體的配置選項,請閱讀官方文件 https://www.typescriptlang.org/docs/handbook/tsconfig-json.html,下面是我自己的配置

{
    "compilerOptions": {
      "target": "esnext",
      "lib": [
        "dom",
        "dom.iterable",
        "esnext"
      ],
      "allowJs": true,
      "skipLibCheck": true,
      "esModuleInterop": true,
      "allowSyntheticDefaultImports": true,
      "strict": true,
      "forceConsistentCasingInFileNames": true,
      "module": "commonjs",
      "moduleResolution": "node",
      "isolatedModules": true,
      "resolveJsonModule": true,
      "noEmit": true,
      "sourceMap": true,
      "declaration": true,
      "noUnusedLocals": false,
      "noUnusedParameters": false,
      "incremental": true,
      "noFallthroughCasesInSwitch": true,
      "noImplicitAny": false,
      "baseUrl": "./src"
    },
    "include": [
      "src/**/*.ts"
    ],
    "exclude": ["node_modules"]
}

(三)建立babel.config.js檔案

babel.config.js 檔案主要用來配置babel,什麼是babel,如何配置它的功能,請參看官方文件 https://babeljs.io/docs/en/config-files

下面是我自己的配置程式碼:

module.exports = {
  "presets": [

    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
    [
      "@babel/preset-typescript"
    ]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime",
      {
        "regenerator": true
      }
    ],
    ["module-resolver", {
      "root": ["./src"]
    }]

  ]
};

(四)建立build/config.js檔案

這個檔案屬於腳手架核心檔案,專案的業務需求、開發功能、資源優化、效能、安全等都在這個檔案中展示(你可以根據需要,使用多個build檔案,這些腳手架檔案將和package.json中的命令配置掛鉤,讓你能夠執行它們)

注意:path.resolve(__dirname, '../dist') 將返回:   /Applications/......./dist

檔案的參考程式碼如下(你可以自由擴充套件和改良它,如何改良,你可以參看Webpack的官網文件 https://webpack.js.org/concepts/ ,目前我們使用Webpack 5以上版本,經過自己的實踐,它在效能上要高於4的版本):

'use strict';

const webpack = require('webpack');
const path = require('path');
const json = require('../package.json');
const moment = require('moment');
const TerserPlugin = require("terser-webpack-plugin");

/*! 
 *************************************
 *  Main configuration
 *************************************
 */
const devMode = process.env.NODE_ENV !== 'production';

const webpackConfig = {
	devtool: devMode ? 'source-map' : false,
    mode: devMode === 'development' ? 'development' : 'production',
	watch: true,
    resolve: {
		fallback: {
		    fs: false
		},
        extensions: ['.js', '.ts'],
    },
	
	entry: {
		'app': path.resolve(__dirname, '../src/index.ts'),
		'app.min': path.resolve(__dirname, '../src/index.ts')
	},
	output: {
	  library: {
		name: 'RootLib',
		type: 'var'
	  },
	  filename: '[name].js',
	  path: path.resolve(__dirname, '../dist'),
	},
	/*
	entry: path.resolve(__dirname, '../src/index.ts'),
	output: {
	  filename: 'app.js',
	  path: path.resolve(__dirname, '../dist'),
	},
	*/
	optimization: {
		minimize: true,
	    minimizer: [

			new TerserPlugin({
				test: /\.min\.js$/i
			}),
		],
		
	},
    module: {
        rules: [
            {
                test: /\.(js|ts)$/,
                loader: 'babel-loader',
                exclude: path.resolve( __dirname, 'node_modules' ),
                options: {  
				  'presets': [
					  '@babel/preset-env',
					  '@babel/preset-typescript'  
				  ]
                }
			},
        ],
		
    },
	plugins: []
	
	
};

// Add souce maps
webpackConfig.plugins.push(
	new webpack.SourceMapDevToolPlugin({
	  filename: '../dist/[file].map',
	})
);

// Adds a banner to the top of each generated chunk.
webpackConfig.plugins.push(
    new webpack.BannerPlugin(`
	My React App

	@source: https://github.com/xizon
	@version: ${json.version} (${moment().format( "MMMM D, YYYY" )})
	@license: MIT
	`)
);

									
/*! 
 *************************************
 *  Exporting webpack module
 *************************************
 */
module.exports = webpackConfig;

有些時候我們的專案會發布到npm,然後直接通過Node安裝並呼叫,或者直接通過 也會報錯
 Uncaught ReferenceError: exports is not defined

 正確的用法:

 
    

(2)並且匯出的變數或者函式需要包含 {},如

import __ from './_core/global';
    export default __;
    export { __ };

(3)使用可以減小編譯後的js體積

["@babel/preset-env", { 
        "targets": {"esmodules": true}
    }],

至此,一個基礎的Webpack5+TypeScript+Jest+ESLint的基礎開發環境就已經完成了,接下去,我們還將深入配置React的環境。

(五)深入搭建React環境

我們只是完成了基礎的功能支援,如果我們需要使用React,那麼我們還將進一步配置腳手架。當然,Vue支援也同理。在這個過程中,某些依賴會產生衝突,或者因為TypeScript的某些配置,會造成編譯錯誤,所以我們需要一些測試和檢查,就能發現這些錯誤,並修正一些配置。

這些程式碼都是我經過測試後的基礎參考示例程式碼(大部分已經修正了編譯錯誤),出現JS程式碼編譯錯誤不要緊,我們只要根據終端的報錯,來找出原因,修正腳手架的配置即可。當然,如果是為了方便,你也可以直接使用React官方提供的 create-react-app 來編寫你的應用程式,某些個性化的需求也需要參考文件增加。如果你的專案個性化需求比較多,可以直接創造自己的腳手架,方便混合其它第三方庫和工具。不依賴與第三方的架構。

接下去我們繼續配置React的支援。

第1步: 安裝React相關的依賴項

後面的步驟,如果需要使用npm命令安裝新的依賴,同理進入目錄

先進入你的專案的目錄

cd /{your_directory}/my-react-app

然後執行命令:

sudo npm install

補全TypeScript對於React介面的支援,安裝:

npm install @types/react --save-dev

安裝完基本的依賴,然後安裝react的依賴(這幾個依賴有什麼作用,可以自己去搜索引擎查詢一下,一般是我專案裡常用的一些依賴項)和繼續配置(react入口檔案也可以是ts檔案)

npm install axios react react-dom [email protected] [email protected]
npm install @babel/preset-react @babel/plugin-proposal-class-properties --save-dev

安裝新的依賴後,package.json 檔案會自動修改。

第2步: 修改build/config.js

為了適配React,我們需要修改webpack配置檔案 build/config.js,修改後的程式碼如下(自己可以對比之前的基礎程式碼):

   context: __dirname, // to automatically find tsconfig.json
    resolve: {
        extensions: ['.js', '.jsx', '.ts', '.tsx', '.scss', '.sass'],
        alias: {
            // specific mappings.
            // Supports directories and custom aliases for specific files when the express server is running, 
             // you need to configure the following files at the same time:
             // 1) `babel.config.js`    --> "plugins": [["module-resolver", {"alias": {...}} ]]
             //  2) `tsconfig.json`      --> "compilerOptions": { "paths": {...} }
             //  3) `package.json`       --> "jest": { "moduleNameMapper": {...} }
             '@': path.resolve(__dirname, '../src')
        }
    },
	
	entry: {
		'app': path.resolve(__dirname, '../src/index.js'),
		'app.min': path.resolve(__dirname, '../src/index.js')
	},

module: {
    rules: [
        {
            test: /\.(js|jsx|ts|tsx)$/,
            loader: 'babel-loader',
            exclude: path.resolve(__dirname, '../node_modules' ),
            options: {  
              'presets': [
                  '@babel/preset-env',
                  '@babel/preset-react',
                  '@babel/preset-typescript',
                    {
                      plugins: [
                        '@babel/plugin-proposal-class-properties'
                      ]
                    }	  
              ]
            }
        },
    ],
    
},

Babel的一些外掛有什麼作用,可以直接去官網多看看文件即可。這裡主要說一下 @babel/plugin-proposal-class-properties 外掛,主要是用來編譯的,可以解決一些靜態類屬性編譯的問題。

第3步: 安裝SASS開發依賴

不論我們使用css-in-js還是外部引用CSS樣式檔案,我們都需要配置SASS外掛,讓其能夠編譯SASS,SCSS檔案,我個人比較喜歡使用SCSS檔案,單獨外部引用,而不是直接css-in-js, 這樣便於我自己維護樣式表。

執行命令

npm install sass-loader node-sass style-loader css-loader [email protected] css-minimizer-webpack-plugin --save-dev

【注意】node-sass(7.0.1)和sass-loader(12.4.0)的版本搭配,無需file-loader就可以提取檔案。會和file-loader衝突,提取的檔案可能無法使用,需要移除file-loader相關配置或者降級到node-sass(4.14.1)和sass-loader(7.1.0)才可搭配file-loader使用

安裝關聯jest單元測試的一個依賴

npm install identity-obj-proxy --save-dev

identity-obj-proxy 外掛使用ES6代理的身份物件, 對模擬CSS模組之類的webpack匯入很有用,它可以解決編譯時匯入CSS的一些錯誤

下面的配置適用於node-sass7.x.x, sass-loader 12.x.x 版本
mini-css-extract-plugin2.5.0版本有bug無法執行
適配CSS檔案,修改webpack檔案 build/config.js自己可以對比之前的基礎程式碼

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");


optimization: {
    minimizer: [
        new MiniCssExtractPlugin({
            // Options similar to the same options in webpackOptions.output
            // both options are optional
            filename: '../dist/[name].css'
        }),
        new CssMinimizerPlugin({
            test:/\.min\.css$/i,
            parallel: true,
            minimizerOptions: {
                preset: [
                    "default",
                    {
                        discardComments: { removeAll: true },
                    },
                ],
            },
        }),

    ],
},
module: {
    rules: [
        {
            test: /\.(sa|sc|c)ss$/,
            include: [
                path.resolve(__dirname, '../src'),
                // Prevent errors in calling the node library: Module parse failed: Unexpected character'@'
                path.resolve(__dirname, '../node_modules'),
            ],
            use: [
	   /**
	  * Note:
	   * You can use `style-loader` to inject CSS into the DOM to generate a final js file
	  */
                {
                    loader: MiniCssExtractPlugin.loader, //Extracts CSS into separate files  ( Step 3 )
                    options: {
                        // you can specify a publicPath here
                        // by default it use publicPath in webpackOptions.output
                          publicPath: '../dist/'

                    }
                },

                {
                    loader: "css-loader",  // interprets @import and url() and will resolve them. ( Step 2 )
                    options: {
                        sourceMap: true
                    }
                },
                {
                    loader: 'sass-loader', // compiles Sass to CSS ( Step 1 )
                    options: {
                        sourceMap: true,
		     sassOptions: {
			  /* (nested | expanded | compact | compressed) */
			  outputStyle: 'expanded'
		     },
                    }

                },
            ]
        },
    ],
},

提示:如果不單獨引用生成的 dist/*.css 檔案,使用js內建的css,刪除 mini-css-extract-plugincss-minimizer-webpack-plugin 相關的配置,然後將樣式loader修改為下面的程式碼(自己可以對比之前的基礎程式碼):

{
    test: /\.(sa|sc|c)ss$/,
    include: [
        path.resolve(__dirname, '../src'),
        // Prevent errors in calling the node library: Module parse failed: Unexpected character'@'
        path.resolve(__dirname, '../node_modules'),
    ],
    use: [
        {
            loader: "style-loader"  // creates style nodes from JS strings ( Step 3 )
        },
        {
            loader: "css-loader",   // interprets @import and url() and will resolve them. 
                                    //(translates CSS into CommonJS) ( Step 2 )
            options: {
                sourceMap: true
            }
        },
        {
            loader: 'sass-loader', // compiles Sass to CSS ( Step 1 )
            options: {
                sourceMap: true,
                sassOptions: {
                    /* (nested | expanded | compact | compressed) */
                    outputStyle: 'expanded'
                },

            }

        },
    ]
},

第4步: 繼續安裝引用檔案的開發依賴

執行命令:

npm install raw-loader glslify-loader json-loader file-loader  --save-dev

注意(1):path.resolve(__dirname, '../dist') 將返回:   /Applications/......./dist

注意(2):mini-css-extract-pluginpublicPath 設定會讓 file-loaderpublicPath 路徑失效,導致檔案重複提取,可以刪除 file-loader 外掛的相關配置(不刪除此配置會導致生成圖片解析錯誤)來解決

適配引用的字型,圖片等檔案,修改webpack檔案build/config.js 自己可以對比之前的基礎程式碼

module: {
    rules: [
        {
            test: /\.(glsl|vs|fs|vert|frag)$/,
            exclude: path.resolve(__dirname, '../node_modules' ),
            use: [
                'raw-loader',
                'glslify-loader'
            ]
        },
        {
            test: /\.json$/,
            use: 'json-loader'
        },
        
      // Note:
      // 1) Compatible with node-sass(4+) and sass-loader(7+)
      // 2) The versions of node-sass (7+) and sass-loader (12+) are matched to extract files without `file-loader`
        {
             test: /\.(png|jpe?g|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
            loader: 'file-loader', 
            options: {
              esModule: false, //change the css path via output
		name (file) {
		    return '[name]_[hash].[ext]'
		},
              outputPath: (url, resourcePath, context) => { //the files from `./src/...` will copy to `./dist/`
                  
                 //original name: path.basename(resourcePath)
                 
                 //fonts
                 if ( resourcePath.indexOf( 'webfonts/' ) >= 0 || resourcePath.indexOf( 'fonts/' ) >= 0 ) {
                     return '../dist/fonts/' + url;
                 }
                  
                 //imags
                 if ( resourcePath.indexOf( 'images/' ) >= 0 || resourcePath.indexOf( 'img/' ) >= 0 ) {
                     return '../dist/mages/' + url;
                 } 
                  
                     
                 return '../dist/misc/' + url;
                
              },
              publicPath: (url, resourcePath, context) => { //the css path of output 
                 
                // If the file is in the root directory, you can leave it empty. If in another directory, 
                // you can write: "/blog". (but no trailing slash)
                const websiteRootDir = '';


                 //fonts
                 if ( resourcePath.indexOf( 'webfonts/' ) >= 0 || resourcePath.indexOf( 'fonts/' ) >= 0 ) {
                     return `${websiteRootDir}/dist/fonts/${url}`;
                 }
                
                 //imags
                 if ( resourcePath.indexOf( 'images/' ) >= 0 || resourcePath.indexOf( 'img/' ) >= 0 ) {
                     return `${websiteRootDir}/dist/images/${url}`;
                 } 
                  
                     
                 return `${websiteRootDir}/dist/misc/${url}`;
                  
                
              }
            }
        }

    ],
},

第5步: 繼續安裝webpack本地開發伺服器開發依賴

執行命令

npm install webpack-dev-server  --save-dev

適配4.x.x以上版本的本地服務,修改webpack檔案build/config.js自己可以對比之前的基礎程式碼

設定 static 引數後,public/index.html 內的檔案路徑可以寫成如:../dist/app.min.css, 使用 localhost:8080/public/index.html 訪問,不設定此引數,則會自動定位到 publicdist 資料夾。可以直接使用 localhost:8080 訪問

程式碼如下:

const WebpackDevServer = require('webpack-dev-server');

/*! 
 *************************************
 *  Listen the server
 *************************************
 */

const server = new WebpackDevServer(compiler, {
	 // After setting the static parameter, the file path in `public/index.html` can be written as: `../dist/app.min.css`
	 static: path.resolve(__dirname, '../' ),
	 hot: true,
	 // Disables a full-screen overlay in the browser when there are compiler errors or warnings.
	 client: {
		 overlay: {
			 warnings: false,
			 errors: true
		 }
	 },

});

server.listen(8080, "localhost", function (err, result) {
	if (err) {
		return console.log(err);
	}

	console.log( 'Listening at http://localhost:8080/');
})

第6步: 修改package.json檔案的eslintConfig和jest部分

修改後的程式碼如下(自己可以對比之前的基礎程式碼

  "jest": {
    "moduleNameMapper": {
      "\\.(css|less|scss|sass)$": "identity-obj-proxy",
      "^@/(.*)": "/src/$1"
    },
  },
 "eslintConfig": {
    "parserOptions": {
      "parser": "@typescript-eslint/parser",
      "ecmaVersion": 2018,
      "sourceType": "module",
      "ecmaFeatures": {
        "jsx": true
      }
    },
    "extends": [
      "plugin:react/recommended",
      "plugin:@typescript-eslint/recommended"
    ],
    "rules": {},
    "settings": {
      "react": {
        "version": "detect"
      }
    }
  },

別名如果指定更多,package.json 檔案的 eslintConfigjest 部分可以寫成:

...
  "jest": {
    "testEnvironment": "jsdom",
    "moduleNameMapper": {
      "\\.(css|less|scss|sass)$": "identity-obj-proxy",
      "^@app.react/config/(.*)": "/src/config/$1",
      "^@app.react/components/(.*)": "/src/client/components/$1",
      "^@app.react/router/(.*)": "/src/client/router/$1",
      "^@app.react/helpers/(.*)": "/src/client/helpers/$1",
      "^@app.react/services/(.*)": "/src/client/services/$1",
      "^@app.react/reducers/(.*)": "/src/client/reducers/$1",
      "^@app.react/pages/(.*)": "/src/client/views/_pages/$1",
      "^@app.react/actions/(.*)": "/src/client/actions/$1",
      "^@app.react/server/(.*)": "/src/server/$1",
      "^@app.react/store/(.*)": "/src/store/$1"
    },
    "transform": {
      "^.+\\.(js|jsx)$": "babel-jest",
      "^.+\\.(ts|tsx)?$": "ts-jest"
    }
  },
…

第7步: 修改tsconfig.json檔案

特別注意:baseUrl, "@/*": ["*"]include,這幾個連帶的設定不能出錯。

參考程式碼如下(自己可以對比之前的基礎程式碼

{
    "compilerOptions": {
      "jsx": "react",
     "baseUrl": "./src",
      "paths": {
        "@/*": ["*"]
        }
    },
    "include": [
      "src/**/*.ts", "src/**/*.tsx"
    ],
}

別名如果指定更多,tsconfig.json 可以寫成:

...
      "baseUrl": "./src",
      "paths": {
        "@app.react/config/*": ["config/*"],
        "@app.react/components/*": ["client/components/*"],
        "@app.react/router/*": ["client/router/*"],
        "@app.react/helpers/*": ["client/helpers/*"],
        "@app.react/services/*": ["client/services/*"],
        "@app.react/reducers/*": ["client/reducers/*"],
        "@app.react/pages/*": ["client/views/_pages/*"],
        "@app.react/actions/*": ["client/actions/*"],
        "@app.react/server/*": ["server/*"],
        "@app.react/store/*": ["store/*"]
      }
    },
…

第8步: 修改babel.config.js檔案

參考程式碼如下(自己可以對比之前的基礎程式碼

module.exports = {
  "presets": [
    [
      "@babel/preset-react"
    ],
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-class-properties"
    ],
    ["module-resolver", {
      "root": ["./src"],
      "alias": {
        "@/": "./src"
      }
    }]

  ]
};

別名如果指定更多,babel.config.js 可以寫成:

...
    ["module-resolver", {
      "root": ["./src"],
      "alias": {
        "@app.react/config": "./src/config",
        "@app.react/components": "./src/client/components",
        "@app.react/router": "./src/client/router",
        "@app.react/helpers": "./src/client/helpers",
        "@app.react/services": "./src/client/services",
        "@app.react/reducers": "./src/client/reducers",
        "@app.react/pages": "./src/client/views/_pages",
        "@app.react/actions": "./src/client/actions",
        "@app.react/server": "./src/server",
        "@app.react/store": "./src/store"
      }
    }]
 
…

第9步: 修改webpack的externals屬性【可選】

根據需要修改webpack的排除資料夾,方便建立npm包釋出的編譯檔案。externals 屬性防止將某些 import 的包(package)打包到 bundle 中,而是在執行時(runtime)再去從外部獲取這些擴充套件依賴(external dependencies)。參考程式碼如下:

	//Exclude react from bundle
	externals: [
		{
			// String
			'react': 'React',
			'react-dom': 'ReactDOM',
		},
		// Function
		function ({ context, request }, callback) {

			// Use the same './_all' path to prohibit loading of general style sheets
			if ( request.indexOf( '@/components/_utils/styles' ) >= 0 ) {
				return callback(null, 'commonjs ' + './_all');
			}

			if ( request.indexOf( '@/components/_utils/_all' ) >= 0 ) {
				return callback(null, 'commonjs ' + './_all');
			}	
			callback();
		},
		// Regex
		/^(jquery|\$)$/i,
	],

第10步: 根據需要定義webpack的外掛【可選】

下面的例子定義了一個編譯完成後的外掛,遍歷目錄並且移動刪除。

/*! 
 *************************************
 *  Run command after webpack build
 *************************************
 */
 const glob = require('glob');
 const fs = require('fs');
 
 
 const packagesRoot = glob.sync( path.resolve(__dirname, '../packages/*/*.ts') );
 const packagesSub = glob.sync( path.resolve(__dirname, '../packages/*.ts') );
 const packages = packagesRoot.concat( packagesSub );
 const componentsEntry = {};
 packages.map( ( path ) => {
     const filename = path.split( '/' ).pop().replace('.ts', '');
     componentsEntry[ filename ] = path;
 });
 
 console.log( 'componentsEntry: ', componentsEntry );

 class MyPluginCompiledFunction {
	// Define `apply` as its prototype method which is supplied with compiler as its argument
	apply(compiler) {
		// Specify the event hook to attach to
		compiler.hooks.done.tap('MyPluginCompiledFunction', (compilation) => {

			//Move the components to folders of root directory
			const comNames = Object.keys( componentsEntry );
			packages.map( ( comPath, index ) => {
				
				const newDir = path.resolve(__dirname, `../${comNames[index]}`);
				const oldPath = path.resolve(__dirname, `../dist/cjs/${comNames[index]}.js`);
				const newPath = `${newDir}/index.js`;
				
				if (!fs.existsSync(newDir)){
					fs.mkdirSync(newDir);
				}

				fs.rename(oldPath, newPath, function (err) {
				  if (err) throw err
				  console.log(`Successfully ${comNames[index]}.js moved!`);
				});

			});

			//remove old folder
			// Where the recursive option deletes the entire directory recursively.
			fs.rmdirSync(path.resolve(__dirname, '../dist/cjs'), { recursive: true });
			
		});
	}
}
  

/*! 
 *************************************
 *  Main configuration
 *************************************
 */
 module.export = {
    ...
	plugins: [ new MyPluginCompiledFunction() ]
};

第11步: 根據需要定義Node指令碼【可選】

指令碼可以單獨分離出來,拋棄webpack,直接執行,使用node來執行JS檔案即可修改 package.json 檔案:

  "scripts": {
    "clear": "node xxx.js"
  }

(六)使用webpack內建生成HTML功能

如果建立Electron應用,需要單獨呼叫生成的js,所以內建生成HTML的功能暫時不使用。這裡做一個配置參考。

您可以手動建立 public/index.html 來載入 dist 裡生成的 app.min.cssapp.min.js,8080埠將預設執行 public 裡的index.html,它將自動定位到dist的檔案,所以無需增加dist的二級目錄。直接寫 app.min.cssapp.min.js 即可

webpack-dev-server 設定 static引數後,public/index.html 內的檔案路徑可以寫成如:../dist/app.min.css, 使用 localhost:8080/public/index.html 訪問)

到目前為止,我們都是在 index.html 檔案中手動引入所有資源,然而隨著應用程式增長,並且一旦開始 在檔名中使用 hash 並輸出 多個 bundle,如果繼續手動管理 index.html 檔案,就會變得困難起來。然而,通過一些外掛可以使這個過程更容易管控。

我們使用webpack的HTML生成功能直接打包生成,並且匯入資源。參看:

https://webpack.docschina.org/guides/asset-management/
https://webpack.docschina.org/guides/output-management/

使用內建的Asset Modules來匯入圖片檔案等,使用 HtmlWebpackPlugin 外掛來生成HTML檔案

(七)結語

好了,我們已經完成了從基礎到深入的基礎腳手架搭建,屬於自己的一個微前端架構算是完成了一個雛形,以其說是雛形,也可以說其實可以直接用於部分適合需求的專案。你也可以再次基礎上增加更多的配置,比如PM2的支援,Express的支援,資源壓縮優化等等功能。

都到這裡了,接下去,我們要做一個專案,我們可以使用自己的腳手架,也可以大膽的使用諸如nextjs或者create-react-app這類的工具了,底氣足了,學習起來是不是更有幹勁了?如果文章對你有幫助,也可以持續關注我,不定期更新。更好的閱讀體驗,可檢視我的個人部落格,感謝閱讀:)