• Comments
  • Comments
  • Comments
  • Comments
  • Comments
  • Comments
  • Comments

理解微前端 - 从部署一套自己的前端开发环境(脚手架)开始

理解微前端 - 从部署一套自己的前端开发环境(脚手架)开始

很久以前的前端,没有太多工具化工程化思想,一堆代码塞进去完事儿。如今前端已经很卷,卷到了一个开发环境都够你折腾一宿。那么我们抛开类似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安装并调用。
(1)注意:如果要使用npm发布的编译包,设置为下面的属性(如果导出的编译包不是为了使用import,则不要使用下面的配置,否则浏览器会报错):

	output: {
		library: {
			type: 'commonjs'
		},
	},

直接使用<script src="global.js" ></script> 也会报错
 Uncaught ReferenceError: exports is not defined

正确的用法:

<script>const exports = {"__esModule": true};</script>
<script src="global.js"></script>

(2)并且导出的变量或者函数需要包含 {},如

import __ from './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 react-router-config@5.1.1 react-router-dom@5.2.0
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 mini-css-extract-plugin@2.4.5 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这类的工具了,底气足了,学习起来是不是更有干劲了?如果文章对你有帮助,也可以持续关注我,不定期更新一个小技巧。

本文出自没位道 - Chuckie Chang个人网站,转载请保留出处,谢谢!
文章采用 CC-BY-4.0 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。


返回列表  分享

分享给朋友!

微信
微博
文章评论