最新公告
  • 欢迎您光临网站无忧模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    正文概述 掘金(CookieBoty)   2021-07-26   485

    前言

    上一篇企业级 CLI 开发中,已经针对构建这块的流程做了一个初级的 CLI,但对于工程化体系的建设仅仅也只是迈出了第一步。

    开发者平常最多的还是在开发业务代码,仅仅依靠 CLI 从 devops 末端去约束是远远不够的,所以一般的小团队也会从脚手架入手。

    本篇将以 React 为例定制一套自定义脚手架以及对之前的 CLI 进行升级。

    自定义 React 脚手架

    脚手架设计一般分为两块,一块是基础架构,一块是业务架构。

    基础架构决定脚手架的技术选型、构建工具选型以及开发优化、构建优化、环境配置、代码约束、提交规范等。

    业务架构则是针对业务模块划分、请求封装、权限设计等等于与业务耦合度更高的模块设计。

    搭建基础架构

    跟 CLI 一样都是从 0 搭建这个脚手架,所以起手还是初始化项目与 ts 配置。

    npm init
    tsx --init
    

    如上先将 package.josntsconfig.json 生成出来,tsconfig.json 的配置项可以直接使用下面的配置或者根据自己需求重新定义。

    {
      "include": [
        "src"
      ],
      "compilerOptions": {
        "module": "CommonJS",
        "target": "es2018",
        "outDir": "dist",
        "noEmit": true,
        "jsx": "react-jsx",
        "esModuleInterop": true,
        "moduleResolution": "node",
        "strict": true,
        "noUnusedLocals": false,
        "noFallthroughCasesInSwitch": true,
        "baseUrl": "./",
        "keyofStringsOnly": true,
        "skipLibCheck": true,
        "paths": {
          "@/*": [
            "./src/*"
          ]
        }
      }
    }
    

    下面是 package.josn 的依赖与一些其他的配置,也一起附上,这里不再针对每个依赖包做单独说明,如果对哪个模块有不理解的地方,可以在留言区评论咨询。

    {
      "name": "react-tpl",
      "version": "1.0.0",
      "description": "a react tpl",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "cross-env NODE_ENV=development webpack-dev-server --config ./script/webpack.config.js",
      },
      "author": "cookieboty",
      "license": "ISC",
      "dependencies": {
        "@babel/cli": "^7.14.5",
        "@babel/core": "^7.14.6",
        "@babel/preset-env": "^7.14.7",
        "@babel/preset-react": "^7.14.5",
        "@babel/preset-typescript": "^7.14.5",
        "babel-loader": "^8.2.2",
        "clean-webpack-plugin": "^4.0.0-alpha.0",
        "cross-env": "^7.0.3",
        "css-loader": "^6.1.0",
        "file-loader": "^6.2.0",
        "html-webpack-plugin": "^5.3.2",
        "less": "^4.1.1",
        "less-loader": "^10.0.1",
        "react": "^17.0.2",
        "react-dom": "^17.0.2",
        "style-loader": "^3.1.0",
        "typescript": "^4.3.5",
        "webpack": "^5.45.1",
        "webpack-cli": "3.3.12",
        "webpack-dev-server": "^3.11.2"
      },
      "devDependencies": {
        "@types/react": "^17.0.14",
        "@types/react-dom": "^17.0.9"
      }
    }
    

    配置 webpack

    新建 script/webpack.config.js 复制下述配置。

    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin')
    const { CleanWebpackPlugin } = require('clean-webpack-plugin')
    
    module.exports = {
      mode: "development",
      entry: "./src/index.tsx",
      devServer: {
        contentBase: path.resolve(__dirname, "dist"),
        hot: true,
        historyApiFallback: true,
        compress: true,
      },
      resolve: {
        alias: {
          '@': path.resolve('src')
        },
        extensions: ['.ts', '.tsx', '.js', '.json']
      },
      module: {
        rules: [
          {
            test: /\.(js|jsx|ts|tsx)$/,
            use: {
              loader: require.resolve('babel-loader')
            },
            exclude: [/node_modules/],
          },
          {
            test: /\.(css|less)$/,
            use: [
              {
                loader: "style-loader",
              },
              {
                loader: "css-loader",
                options: {
                  importLoaders: 1,
                },
              },
            ],
          },
          {
            test: /\.(png|svg|jpg|gif|jpeg)$/,
            loader: 'file-loader'
          },
          {
            test: /\.(woff|woff2|eot|ttf|otf)$/,
            loader: 'file-loader'
          }
        ],
      },
      plugins: [
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
          template: 'tpl/index.html'
        }),
      ]
    };
    

    这里有个需要注意的点是 webpack-cliwebpack-dev-server版本需要保持一致,都是用 3.0 的版本即可,如果版本不一致的话,会导致报错。

    配置 React 相关

    新建 tpl/index.html 文件(html 模板),复制下述代码

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8"/>
      </head>
      <body>
        <div id="root"></div>
      </body>
    </html>
    

    新建 src/index.tsx 文件(入口文件),复制下述代码

    import React from "react";
    import ReactDOM from "react-dom";
    import App from "./App";
    
    ReactDOM.render(
      <App />,
      document.getElementById("root")
    );
    

    新建 .babelrc 文件(babel 解析配置),复制下述代码

    {
      "presets": [
        "@babel/preset-env",
        "@babel/preset-react",
        [
          "@babel/preset-typescript",
          {
            "isTSX": true,
            "allExtensions": true
          }
        ]
      ]
    }
    

    完成上述一系列配置之后,同时安装完依赖之后,运行 yarn start,此时应该是能够正常运行项目如下图所示

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    浏览器打开 http://localhost:8081/,即可看到写出来的展示的页面

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    至此,已经完成了一个初步的脚手架搭建,但是针对于业务来说,还是有很多的细节需要完善。接下来,我们一起针对平常开发需要使用到的模块对项目进行进一步的配置。

    优化 Webpck Dev 配置

    简化 server 信息输出

    前面的配图可以看出 webpack-dev-server 输出的信息很乱,可以使用 Stats 配置字段对输出信息进行过滤。

    一般我们只需要看到 error 信息即可,可以添加如下参数:

    devServer: {
        stats: 'errors-only', // 过滤信息输出
        contentBase: path.resolve(__dirname, "dist"),
        hot: true,
        historyApiFallback: true,
        compress: true,
    },
    

    添加构建信息输出

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    ProgressPlugin 可以监控各个 hook 执行的进度 percentage,输出各个 hook 的名称和描述。

    使用也非常简单,按照如下引用之后,就可以正常输出如图标红的构建进度。

    const { ProgressPlugin } = require('webpack')
    plugins: [
        ...
        new ProgressPlugin(),
    ]
    

    优化业务模块

    先将项目目录划分好,约定好每个目录的文件的作用与功能。

    这里的规范并不是一定的,具体要看各个团队自己的开发规范来定制,例如有的团队喜欢将公共的资源放在 public 目录等。

    ├── dist/                          // 默认的 build 输出目录
    └── src/                           // 源码目录
        ├── assets/                    // 静态资源目录
        ├── config                     
            ├── config.js              // 项目内部业务相关基础配置
        ├── components/                // 公共组件目录
        ├── service/                   // 业务请求管理
        ├── store/                     // 共享 store 管理目录
        ├── util/                      // 工具函数目录
        ├── pages/                     // 页面目录
        ├── router/                    // 路由配置目录
        ├── .index.tsx                 // 依赖主入口
    └── package.json
    

    配置路由

    收敛路由的好处是可以在一个路由配置文件查看到当前项目的一个大概情况,便于维护管理,当然也可以使用约定式路由,即读取 pages 下文件名,根据文件命名规则来自动生成路由。但这种约束性我感觉还是不太方便,个人还是习惯自己配置路由规则。

    首先改造 index.tsx 入口文件,代码如下:

    import React from 'react'
    import ReactDOM from 'react-dom'
    import { HashRouter, Route, Switch } from 'react-router-dom'
    import routerConfig from './router/index'
    import './base.less'
    
    ReactDOM.render(
      <React.StrictMode>
        <HashRouter>
          <Switch>
            {
              routerConfig.routes.map((route) => {
                return (
                  <Route key={route.path} {...route} />
                )
              })
            }
          </Switch>
        </HashRouter>
      </React.StrictMode>,
      document.getElementById('root')
    )
    

    router/index.ts 文件配置,代码如下:

    import BlogsList from '@/pages/blogs/index'
    import BlogsDetail from '@/pages/blogs/detail'
    
    export default {
      routes: [
        { exact: true, path: '/', component: BlogsList },
        { exact: true, path: '/blogs/detail/:article_id', component: BlogsDetail },
      ],
    }
    
    

    Service 管理

    跟收敛路由是一样的意思,收敛接口也可以统一修改、管理这些请求,如果有复用接口修改可以从源头处理。

    所有项目请求都放入 service 目录,建议每个模块都有对应的文件管理,如下所示:

    import * as information from './information'
    import * as base from './base'
    
    export {
      information,
      base
    }
    

    这样可以方便管理请求,base.ts 作为业务请求类,可以在这里处理一些业务特殊处理。

    import { request } from '../until/request'
    
    const prefix = '/api'
    
    export const getAllInfoGzip = () => {
      return request({
        url: `${prefix}/apis/random`,
        method: 'GET'
      })
    }
    
    

    util/request 作为统一引入的请求方法,可以自行替换成 fetch、axios 等请求库,同时可以在此方法内封装通用拦截逻辑。

    import qs from 'qs'
    import axios from "axios";
    
    interface IRequest {
        url: string
        params?: SVGForeignObjectElement
        query?: object
        header?: object
        method?: "POST" | "OPTIONS" | "GET" | "HEAD" | "PUT" | "DELETE" | undefined
    }
    
    interface IResponse {
        count: number
        errorMsg: string
        classify: string
        data: any
        detail?: any
        img?: object
    }
    
    export const request = ({ url, params, query, header, method = 'POST' }: IRequest): Promise<IResponse> => {
        return new Promise((resolve, reject) => {
            axios(query ? `${url}/?${qs.stringify(query)}` : url, {
                data: params,
                headers: header,
                method: method,
            })
                .then(res => {
                    resolve(res.data)
                })
                .catch(error => {
                    reject(error)
                })
        })
    }
    

    具体通用拦截,请参考 axios 配置,或者自己改写即可,需要符合自身的业务需求。

    在具体业务开发使用的时候可以按照模块名引入,容易查找对应的接口模块。

    import { information } from "@/service/index";
    
    const { data } = await information.getAllInfoGzip({ id });
    

    上述是针对项目做了一些业务开发上的配置与约定,各位同学可以根据自己团队中的规定与喜好行修改。

    CLI 升级改造

    在上述自定义 React 脚手架搭建完毕之后,我们如果直接用使用上一篇搭建出来的 CLI 来构建项目是不会构建成功的,还有印象的同学,应该记得之前的 CLI 的入口文件是 src/index.js,html 模板使用的是 public/index.html

    很明显可以看出,此时的 CLI 是远远达不到要求的,我们并不能在每一次开发的时候都需要对 CLI 进行更新,这样是违背 CLI 的通用性原则。

    那么该如何解决这个问题呢?

    自定义配置文件

    根目录新建 cli.config.json 文件,此文件将是需要读取配置的文件。

    将此项目的自义定配置写入文件,供给 CLI 读取。

    {
      "entry": {
        "app": "./src/index.tsx"
      },
      "output": {
        "filename": "build.js",
        "path": "./dist"
      },
      "template": "tpl/index.html"
    }
    

    CLI 同步进行改造,代码如下:

    require('module-alias/register')
    import webpack from 'webpack';
    import { getCwdPath, loggerTiming, loggerError } from '@/util'
    import { loadFile } from '@/util/file'
    import { getProConfig } from './webpack.pro.config'
    import ora from "ora";
    
    export const buildWebpack = () => {
    
      const spinner = ora('Webpack building...')
    
      const rewriteConfig = loadFile(getCwdPath('./cli.config.json')) // 读取脚手架配置文件
    
      const compiler = webpack(getProConfig(rewriteConfig));
    
      return new Promise((resolve, reject) => {
        loggerTiming('WEBPACK BUILD');
        spinner.start();
        compiler.run((err: any, stats: any) => {
          console.log(err)
          if (err) {
            if (!err.message) {
              spinner.fail('WEBPACK BUILD FAILED!');
              loggerError(err);
              return reject(err);
            }
          }
        });
    
        spinner.succeed('WEBPACK BUILD Successful!');
        loggerTiming('WEBPACK BUILD', false);
      })
    }
    

    webpack.pro.config.ts 代码如下:

    import getBaseConfig from './webpack.base.config'
    import { getCwdPath, } from '@/util'
    
    interface IWebpackConfig {
      entry: {
        app: string
      }
      output: {
        filename: string,
        path: string
      }
      template: string
    }
    
    export const getProConfig = (config: IWebpackConfig) => {
      const { entry: { app }, template, output: { filename, path }, ...rest } = config
    
      return {
        ...getBaseConfig({
          mode: 'production',
          entry: {
            app: getCwdPath(app || './src/index.js')
          },
          output: {
            filename: filename || 'build.js',
            path: getCwdPath(path || './dist'), // 打包好之后的输出路径
          },
          template: getCwdPath(template || 'public/index.html')
        }),
        ...rest
      }
    }
    

    通过 loadFile 函数,读取脚手架自定义配置项,替换初始值,再进行项目构建,构建结果如下:

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    接管 dev 流程

    与接管构建流程类似,在我们进行自定义脚手架构建之后,可以以此为基础将项目的 dev 流程也接管,避免项目因为开发与构建的依赖不同而导致构建失败,从源头管理项目的规范与质量。

    在前面脚手架中配置的 webpack-dev-server 是基于 webpack-cli 来使用的。

    既然使用 CLI 接管 dev 环境,那么也就不需要将 webpack-dev-server 作为 webpack插件使用,而是直接调用 webpack-dev-serverNode Api

    将刚刚的脚手架的 webpack-dev-server 配置抽离,相关配置放入 CLI 中。

    const WebpackDevServer = require('webpack-dev-server/lib/Server')
    
    export const devWebpack = () => {
      const spinner = ora('Webpack running dev ...')
    
      const rewriteConfig = loadFile(getCwdPath('./cli.config.json'))
      const webpackConfig = getDevConfig(rewriteConfig)
    
      const compiler = webpack(webpackConfig);
    
      const devServerOptions = {
        contentBase: 'dist',
        hot: true,
        historyApiFallback: true,
        compress: true,
        open: true
      };
      
      const server = new WebpackDevServer(compiler, devServerOptions);
    
      server.listen(8000, '127.0.0.1', () => {
        console.log('Starting server on http://localhost:8000');
      });
    }
    

    然后在脚手架的 package.json scripts 添加对应的命令就可以完成对 dev 环境的接管,命令如下:

    "scripts": {
         "dev": "cross-env NODE_ENV=development fe-cli webpack",
         "build": "cross-env NODE_ENV=production fe-cli webpack"
     }
    

    运行对应的命令即可运行或者打包当前脚手架内容。

    优化 webpack 构建配置

    上一篇就已经介绍过了,目前的构建产物结果很明显并不是我们想要的,也不符合普通的项目规范,所以需要将构建的配置再优化一下。

    mini-css-extract-plugin

    mini-css-extract-plugin 是一款样式抽离插件,可以将 css 单独抽离,单独打包成一个文件,它为每个包含 css 的 js 文件都创建一个 css 文件。也支持 css 和 sourceMaps 的按需加载。配置代码如下:

    {
        rules: [
            test: /\.(css|less)$/,
                use: [MiniCssExtractPlugin.loader],
              }
        ]
    }
      
    plugins: [
          new MiniCssExtractPlugin({
            filename: '[name].[contenthash].css',
            chunkFilename: '[id].[contenthash].css',
            ignoreOrder: true,
          })
        ]
    

    提取公共模块

    我们可以使用 webpack 提供的 splitChunks 功能,提取 node_modules 的公共模块出来,在 webpack 配置项中添加如下配置即可。

     optimization: {
          splitChunks: {
            cacheGroups: {
              commons: {
                test: /[\\/]node_modules[\\/]/,
                name: 'vendors',
                chunks: 'all',
              },
            },
          },
    },
    

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    如图,现在构建出来的产物是不是瞬间清晰多了。

    优化构建产物路径

    上述的构建产物虽然已经优化过了,但是目录依然还不够清晰,我们可以对比下图的 cra 构建产物,然后进行引用路径的优化。

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    其实很简单,将所有构建产物的路径前面统一添加 static/js,这样在进行构建得到的产物就如下图所示。

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    配置增量构建(持久化缓存)

    这是 webpack 5 的新特性,在 webpack 4 的时候,我们常用优化构建的手段是使用 hard-source-webpack-plugin 这个插件将模块依赖缓存起来,再第二次构建的时候会直接读取缓存,加快构建速度。

    这个过程在 webpack 5 里面被 cache 替代了,官方直接内置了持久化缓存的功能,配置起来也非常方便,添加如下代码即可:

    import { getCwdPath } from '@/util'
    
    export default {
      cache: {
        type: 'filesystem',  //  'memory' | 'filesystem'
        cacheDirectory: getCwdPath('./temp_cache'), // 默认将缓存存储在 当前运行路径/.cache/webpack
        // 缓存依赖,当缓存依赖修改时,缓存失效
        buildDependencies: {
          // 将你的配置添加依赖,更改配置时,使得缓存失效
          config: [__filename]
        },
        allowCollectingMemory: true,
        profile: true,
      },
    }
    

    然后在运行构建或者开发的时候,会在当前运行目录生产缓存文件如下:

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    现在让我们一起来看看,构建速度的提升有多少:

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    可以很明显看出,第一构建速度比之前要慢 2s 左右,但是第二次构建速度明显提升,毕竟脚手架目前的内容太少了,初次构建使用增量的时候会比普通编译多了存储缓存的过程。

    这里有个需要注意的点,因为我们是调用 webpack 的 Node Api 来构建,所以需要显示关闭 compiler 才能正常生产缓存文件。

    const compiler = webpack(webpackConfig);
    
      try {
        compiler.run((err: any, stats: any) => {
    
          if (err) {
            loggerError(err);
          } else {
            loggerSuccess('WEBPACK SUCCESS!');
          }
          compiler.close(() => {
            loggerInfo('WEBPACK GENERATE CACHE'); // 显示调用 compiler 关闭,生成缓存
          });
          loggerTiming('WEBPACK BUILD', false);
        });
      } catch (error) {
        loggerError(error)
      }
    

    特别鸣谢

    前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    这是上一篇的读者留言,此处@琦玉,感谢这位同学的建议,后面的系列博文除了介绍思路之外,coding 与步骤会更加详细,也会及时提供项目 demo 供给参考,其他同学更好的建议也可以在评论区反馈。希望除了能将这个系列写完之外,还能写得更好,让我能和更多的同学一起互相学习、共同成长。

    写在最后

    CLI 工具到此为止,总算是有个大概可用的雏形了,但是作为企业级的 CLI 目标,我们还差很长的一段路要走,仅仅构建这块能优化的点就非常多,包括但不限于构建配置的约束、拓展、提交约束等等细节性的优化。

    所有的项目代码已经上传至项目地址,有兴趣的同学可以拉取参考,后续所有专栏的相关的代码都会统一放在 BOTY DESIGN 中。


    下载网 » 前端工程化实战 - 自定义 React 脚手架 & CLI 升级

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元