如何打造一个react技术栈的多页面的框架应用

更新日期: 2021-08-12阅读: 1.9k标签: 多页面

现在很多脚手架基本上都是单页面入口的应用,比如create-react-app等,整个的入口就是src/index.js。

即使是Next.js,可以使用next export,导出成一个个单独的 html 页面。但在编译时,依然是全量编译,作为本身是为同构直出量身定制的服务端框架,在导出成纯前端运行的页面时,会多出很多无用的代码

我已将改造完成的框架放在了GitHub上,您可以直接下载下来进行体验:https://github.com/wenzi0github/create-react-app-mpa

1. 现状

我们业务的主要特点是:

  1. 更偏向于活动,每个活动都搭建一个脚手架和对接构建流水线,很麻烦;

  2. 每个活动 h5 都有单独定制的部分,且用户交互也多,只能人工开发;

  3. 希望把 Git 仓库进行聚合,避免分散在各个位置;

因此,这里我基于create-react-app进行改造,实现后的功能有:

  1. 命令行快速初始化一个项目或者页面;

  2. 统一代码规范,接入公司的 eslint 代码检查;

  3. 每次提交后,只构建当前发生修改的项目;

  4. 测试环境和预发布环境自动插入 vconsole 等调试工具和构建时间;

  5. 兼容之前已构建的 html 页面,因之前是采用纯 html 发布的,这里还要兼容一下的(我们部门独有的方式,各位可以忽略);

2. 改造流程

接下来按照我的步骤,一步步下来,就能顺利的搭建起来了。

2.1 创建项目

我们首先使用 create-react-app 初始化一个项目。我比较喜欢使用 typescript,因此可以:

npx create-react-app my-app --template typescript

# or

yarn create react-app my-app --template typescript

进入到项目my-app中,因脚手架将配置隐藏了,改造时,需使用npm run eject命令释放出配置。

注意,这是一个单项操作,执行后所有的配置就不能再收回去了。
$ npm run eject

执行后可以看到多了 config 和 scripts 两个文件夹,我们几乎所有的改动都在这两个里面。

不同的版本,在配置上可能稍微有点区别。

2.2 添加 sass 和 less

sass 的配置是create-react-app本身已配置好,但还需要自己下载一下sass这个包:

$ npm i sass --save-dev

# or

$yarn add sass

对于 less,我们就需要自己配置一下,先安装 less 和 less-loader,然后直接参考 sass 的配置即可,在config/webpack.config.js中。

安装 less:

$ npm install less less-loader --save-dev

配置:

// config/webpack.config.js

// 64行左右
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
+ const lessRegex = /\.less$/; // 新增less的后缀名
+ const lessModuleRegex = /\.module\.less$/; // 新增less的后缀名

// 538行左右
rules: [
  // 紧接着sass的配置
  {
    test: lessRegex,
    exclude: lessModuleRegex,
    use: getStyleLoaders(
      {
        importLoaders: 3,
        sourceMap: isEnvProduction
          ? shouldUseSourceMap
          : isEnvDevelopment,
      },
      'less-loader'
    ),
    sideEffects: true,
  },
  {
    test: lessModuleRegex,
    use: getStyleLoaders(
      {
        importLoaders: 3,
        sourceMap: isEnvProduction
          ? shouldUseSourceMap
          : isEnvDevelopment,
        modules: {
          getLocalIdent: getCSSModuleLocalIdent,
        },
      },
      'less-loader'
    ),
  },
]

这样项目中 sass 和 less 都可以支持了。

2.3 多入口的配置

在config/paths.js中,有各种的配置,这里我们需要添加几个根据项目目录构建的配置。

// config/paths.js

module.exports = {
  // build后的目录
  appProjectBuild: (project) => {
    if (project) {
      return resolveApp(`dist/${project.project}`);
    }
    return resolveApp('dist');
  },
  // 之前是指定的src/index的文件
  // 现在是读取src/pages中的项目的文件名
  appProjectIndexJs: (project) => resolveModule(resolveApp, `src/pages/${project.project}/${project.name}`), // 项目的入口文件
};

这里我们添加了 2 个配置,一个是项目打包时的构建目录,一个是构建的入口文件。

在config/webpack.config.js中的导出,是一个函数,这里是我们主要改造的地方。

  1. 按照传入的 project 设置 entery 字段和 output 字段;

  2. 添加postcss-px-to-viewport插件,将 px 转为 vw 单位;

  3. 设置 alias;

  4. 删除不必要的 service worker(如果需要的话,那可以不删除);

在多入口的配置上,本地开发和线上发布的是不一样的,这里我们通过变量 isEnvProduction 进行了区分。在本地开发过程中,只能同时启动一个项目,因此会把该项目中所有的页面都作为入口文件;而在发布的过程中,则是通过判断项目是否变动,来进行构建,因此这里没有限定构建项目,而是可以根据变动的项目,产生多个入口。

2.3.1 设置入口和出口路径

默认的 entry 和 output 都是固定的,我们要修改为与启动和打包的项目相关。

// config/webpack.config.js

// 添加第2参数project
module.exports = function (webpackEnv, project) {
  /**
   * 获取入口
   * @param {object} entryPages 当前项目所有的页面
   * @returns {object}
   */
  const getEntries = (entryPages) => {
    if (isEnvProduction) {
      return [paths.appProjectIndexJs(entryPages)];
    }
    const entries = {};
    entryPages.forEach((page) => {
      entries[page.name] = !shouldUseReactRefresh
        ? [webpackDevClientEntry, paths.appProjectIndexJs(page)]
        : paths.appProjectIndexJs(page);
    });
    return entries;
  };

  /**
   * 根据页面名称生成htmlplugins
   * @param {string[]} pages 项目所有的页面名称
   * @returns {any[]}
   */
  const getHtmlPlugins = (pages) => {
    if (isEnvProduction) {
      return [
        new HtmlWebpackPlugin(
          Object.assign(
            {},
            {
              title: pages.config.title,
              inject: true,
              template: paths.appHtml,
              filename: `${pages.name}.html`,
              compiledTime: new Date().toLocaleString(),
              projectname: pages.config.project,
              minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
              },
            },
          ),
        ),
      ];
    }
    return pages.map((page) => {
      return new HtmlWebpackPlugin(
        Object.assign(
          {},
          {
            title: page.config.title,
            inject: true,
            projectname: page.config.project,
            template: paths.appHtml,
            filename: `${page.name}.html`,
            chunks: [page.name],
          },
        ),
      );
    });
  };

  return {
    entry: getEntries(project),
    output: {
      path: isEnvProduction ? paths.appProjectBuild(project) : undefined,
    },
  };
};

在 public/index.html 中,我们发现有使用到htmlWebpackPlugin.options.title的变量,这是使用HtmlWebpackPlugin插件定义的。

我还自己定义了一个变量compiledTime,用来标识该项目的编译时间。

然后我在 index.html 中作为 meta 标签来使用:

<meta name="createtime" content="<%= htmlWebpackPlugin.options.compiledTime %>" />

当然,有些同学喜欢用注释的方式,也是可以的:

<!-- createtime: <%= htmlWebpackPlugin.options.compiledTime %> -->

2.3.2 添加postcss-px-to-viewport插件

我个人在开发中,喜欢使用vw和vh单位,来开发移动端的应用,那么postcss-px-to-viewport插件就是必然要安装的。

$ npm install postcss-px-to-viewport --save-dev

然后在postcss-loader中的 options.plugins 里添加上这个插件:

// config/webpack.config.js

// 读取viewportWidth的配置,这个后面要讲
// 正式环境是按照页面构建的,project表示是一个页面的配置
// 本地环境是按照项目构建的,project是页面的数组集合
const { viewportWidth } = isEnvProduction ? project.config : project[0].config;


const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    // 忽略其他代码
    {
      loader: require.resolve('postcss-loader'),
      options: {
        // Necessary for external CSS imports to work
        // https://github.com/facebook/create-react-app/issues/2677
        ident: 'postcss',
        plugins: () => [
          require('postcss-flexbugs-fixes'),
          require('postcss-preset-env')({
            autoprefixer: {
              flexbox: 'no-2009',
            },
            stage: 3,
          }),
+         require('postcss-px-to-viewport')({
+           viewportWidth: viewportWidth || 750,
+           unitPrecision: 3,
+           viewportUnit: 'vw',
+         }),
          // Adds PostCSS Normalize as the reset css with default options,
          // so that it honors browserslist config in package.json
          // which in turn let's users customize the target behavior as per their needs.
          postcssNormalize(),
        ],
        sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
      },
    },
  ];
  // 忽略其他代码
};

2.3.3 设置 alias

我们可以设置根路径,避免每次都按照相对路径来进行引用。

这里要配置 2 个文件,一个是 config/webpack.config.js,另一个是 tsconfig.json。

config/webpack.config.js:

// config/webpack.config.js

// 直接查找resolve.alias的位置
module.exports = function (webpackEnv, project) {
  /* 忽略其他代码 */
  return {
    resolve: {
      alias: {
        // Support React Native Web
        // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
        'react-native': 'react-native-web',
+       '@': path.resolve(__dirname, '..', 'src'), // 以src目录为起始的根目录
        // Allows for better profiling with ReactDevTools
        ...(isEnvProductionProfile && {
          'react-dom$': 'react-dom/profiling',
          'scheduler/tracing': 'scheduler/tracing-profiling',
        }),
        ...(modules.webpackAliases || {}),
      },
    },
  };
};

tsconfig.json:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

这样就可以通过@来引用了:

import api from '@/common/config/api'; // 项目目录/src/common/config/api

2.3.4 删除不必要的代码

有些代码是我们项目中用不到的,这里进行了删除处理,如果您的项目用到的话,那就别删除了。

删除 service worker:

// Generate a service worker script that will precache, and keep up to date,
// the HTML & assets that are part of the webpack build.
isEnvProduction &&
  fs.existsSync(swSrc) &&
  new WorkboxWebpackPlugin.InjectManifest({
    swSrc,
    dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
    exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
    // Bump up the default maximum size (2mb) that's precached,
    // to make lazy-loading failure scenarios less likely.
    // See https://github.com/cra-template/pwa/issues/13#issuecomment-722667270
    maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
  }),

2.3.5 关于 manifest 的配置

在 webpack.config.js 中有一个关于ManifestPlugin的配置,这里之前一直没管。每次启动项目时,就会卡在

Starting the development server

的位置。既不报错,也启动不起来。

后来查询了一下,才知道是ManifestPlugin这里配置错了。

我这里呢,是直接删除了,确实没有用到。如果要用的话,应该怎么修改呢?

entrypoints.main开始时,是一个数组,可以执行filter()方法。但修改上面的 webpack 配置后,entrypoints 不再一定包含 main 属性,如图。


因此这里修改为:

let list = [];

// 获取到entrypoints所有的数组
for (const key in entrypoints) {
  list = list.concat(entrypoints[key]);
}
// 文件去重,并筛选掉.map结尾的文件
const entrypointFiles = [...new Set(list)].filter((fileName) => !fileName.endsWith('.map'));

GitHub 上对应的改动 commit:https://github.com/wenzi0github/create-react-app-mpa/commit/16c6de467d8b18faa229b67855cf4014e7988c0a

2.4 配置命令行

所有执行的命令,执行文件均放在scripts目录中,然后在package.json中进行配置。

2.4.1 添加 commit 信息的检查

安装组件工具 commitlint 和 husky

首先按照 commitlint 组件,我们在上面的 2.2.2 小节已经安装安装了 husky,若没有安装时,这里也要安装下 husky。

# Install commitlint cli and conventional config
npm install --save-dev @commitlint/{config-conventional,cli}
# For Windows:
npm install --save-dev @commitlint/config-conventional @commitlint/cli

# Configure commitlint to use conventional config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js

执行后,会在项目根目录生成 commitlint.config.js 的配置文件。

然后将 commitlint 添加到 husky 的配置中:

# Add hook
npx husky add .husky/commit-msg 'npx --no-install commitlint --edit "$1"'

最后我们要添加上我们的规则,这里要修改commitlint.config.js文件了。

首先所有的 commit 信息都要包含下面的关键词:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新功能(feature)
        'fix', // 修补bug
        'docs', // 文档(documentation)
        'style', // 格式(不影响代码运行的变动)
        'refactor', // 重构(即不是新增功能,也不是修改bug的代码变动)
        'test', // 增加测试
        'chore', // 构建过程或辅助工具的变动
        'revert', // 回滚
      ],
    ],
  },
};

接着我们要添加自定义规则,约定每个 commit 信息都需要包含 tapid id:

/* eslint-disable */
module.exports = {
  extends: ['@commitlint/config-conventional'],
  plugins: [
    {
      rules: {
        'need-tapd': (params) => {
          // http://km.oa.com/group/39598/articles/show/440469
          // 提交规范的文档:https://iwiki.woa.com/pages/viewpage.action?pageId=172232748
          const { subject, body, footer } = params;
          const reg = /(--bug=|--story=|--task=|--issue=)(\d+)(\s|$)/;
          if (reg.test(subject) || reg.test(body) || reg.test(footer)) {
            // 只要任意一个位置包含了tapd的相关信息即可
            return [true];
          }
          // 若没有关联tapd,则返回错误,并进行提示
          return [
            false,
            'should contain --story or --bug or --task or --issue\n' +
              'see https://iwiki.woa.com/pages/viewpage.action?pageId=172232748',
          ];
        },
      },
    },
  ],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新功能(feature)
        'fix', // 修补bug
        'docs', // 文档(documentation)
        'style', // 格式(不影响代码运行的变动)
        'refactor', // 重构(即不是新增功能,也不是修改bug的代码变动)
        'test', // 增加测试
        'chore', // 构建过程或辅助工具的变动
        'revert', // 回滚
      ],
    ],
    'need-tapd': [2, 'always'], // 添加上我们的自定义规则
  },
};

若您还有其他的规则,则接着在 plugins.rules 中继续添加即可。

2.4.2 启动某个单独的项目

我们是一个多项目的应用,本地开发时,肯定是要启动一个或几个项目,而不能把所有的项目都启动起来。

这里我们写了一个函数,通过传入的参数,来启动对应的项目。

/* eslint-disable */
const chalk = require('react-dev-utils/chalk');
const glob = require('glob');
const path = require('path');
const getConfigByConfig = require('./getConfig');

const noFileMsg = (project) => `start error, the reason may be:\n
1. project enter error, please ensure you right;
2. in pages/${project} has not .tsx file, at least one .tsx fle\n`;

// 在启动和构建之前执行的代码
// 主要用于检查输入的命令和读取配置文件
// feature-h5/card
const beforeFn = async () => {
  const argv = process.argv.slice(2); // 获取传入的参数
  const [project] = typeof argv === 'object' && argv.length ? argv : [''];

  if (project.length) {
    const list = glob.sync(`src/pages/${project}/*.tsx`).filter((item) => !item.includes('test.ts')); // 获取该项目中所有的入口文件

    if (list.length === 0) {
      // 若该项目没有入口文件,则进行提示
      console.log(chalk.red(noFileMsg(project)));
      process.exit(1);
    }
    const ss = list
      .map((item) => {
        const { ext, name } = path.parse(item);
        if (ext === '.tsx') {
          const config = getConfigByConfig(`src/pages/${project}/${name}.json`);

          return {
            name,
            project,
            config,
            src: item,
          };
        }
        return null;
      })
      .filter(Boolean);
    return Promise.resolve(ss);
  } else {
    // 命令不正确
    console.log(chalk.red(`please start like: \`npm run start home\`, or \`npm run start --page home\`\n`));
    process.exit(1);
  }
};

module.exports = beforeFn;

在scripts/start.js中,我们来调用这个 beforeFn 函数,获取要启动的项目:

beforeFn()
  .then((projects) => {
    // project = [
    //   {
    //     name: 'index',
    //     project: 'home',
    //     src: 'src/pages/home/index.tsx'
    //   },
    //   {
    //     name: 'share',
    //     project: 'home',
    //     src: 'src/pages/home/share.tsx'
    //   }
    // ]
    startCheckBrowsers(projects);
  })
  .catch((err) => {
    console.log(err);
    process.exit(1);
  });

启动时,执行相应的命令即可,例如要启动 demo 项目时:

$ npm run start demo

2.4.3 如何只构建发生变动的项目

在流水线中构建时,如何只构建发生变动的项目?

要完成这个功能,需要解决两个问题。

  1. 如何缓存已构建好的构建产物;

  2. 如何判断当前发生变动的项目;

先来说说如何缓存已构建好的构建产物。

只构建发生变动的项目的前提,是可以缓存已构建项目的构建产物,然后可以将本次的构建产物与之前的构建产物进行合并。然后一并发到服务器上。无论您是将构建好的 html 发到 cdn 上,还是存储到数据库中,还是其他方式(比如我们内部的蓝盾流水线可以缓存构建产物),都是可以的。

但若解决不了这个问题,则无法只构建当前发生变动的项目,因为下次的发布,会将之前已构建好的项目给顶掉。

否则,您只能每次都得把所有的项目构建一遍。不过这种方式,一是没有必要,再有就是浪费时间。毕竟之前的项目没发生变动。

第 2 个问题是如何判断当前发生变动的项目。

我们公司是通过 Git 网站提供的接口,拿两个 commit id 获取到本地改动的文件,然后判断出哪个项目发生了变动,则构建该项目。若您没有这种条件,还有一种方式是,您当前分支的名称跟您要构建的项目相关,然后构建时,获取这个分支的名称。

当然,您可以根据您公司现有的基础功能来决定采用什么方式。

这里默认您已经有可以缓存构建产物的方式和了。

完成获取所有发生变动项目的函数后,就可以在script/build.js中调用他了。

try {
  const entries = await checkFn('npm run build');
  let allResult = [];
  for (let key in entries) {
    const projects = entries[key].projects;
    // console.log(projects)
    if (projects.length) {
      const result = projects.map((item) => {
        return startCheckBrowser(item);
      });
      allResult = allResult.concat(result);
      Promise.all(result).then(() => {
        console.log(`${projects[0].project} all builded`);
        console.log(`>> step 2: ${projects[0].project} has been builded`);
      });
    }
  }
  await Promise.all(allResult);
  console.log(chalk.green(`>> step 3: all pages files builded success`));

  console.log('>> step 4: starting upload static files');
  // 构建完成后,则上传静态资源
  uploadStatic();
  console.log(`>> step 4.6: all files upload success`);
} catch (err) {
  console.log(err);
  process.exit(1);
}

3. 总结

在把create-react-app改造成多页面的应用过程中,最重要的是对 webpack 配置的改造。

原文地址:https://www.xiabingbao.com/post/react/create-react-app-multiple-page-qxphi7.html
博客地址:http://www.xiabingbao.com


链接: https://fly63.com/article/detial/10643

webpack 构建多页面应用初探

如何使用webpack构建多页面应用,这是一个我一直在想和解决的问题。网上也给出了很多的例子,很多想法。猛一看,觉得有那么点儿意思,但仔细看也就那样。使用webpack这个构建工具,可以使我们少考虑很多的问题。

构建多页面应用——静态资源的处理

图片常见的类型有jp(e)g,png,gif,包括现在普遍使用的svg以及webp。svg作为矢量图形,有一定的使用场景,而webp作为未来web开发的趋势,有一定的使用场景,比如:爱奇艺中的轮播图(carousel)中就是用webp,但其他的见到的不多。

vue.js多页面开发环境搭建

利用 vue-cli 搭建的项目大都是单页面应用项目,对于简单的项目,单页面就能满足要求。但对于有多个子项目的应用,如果创建多个单页面

vue多页面项目使用全局Loading组件

多页面vue应用中,在请求接口之前统一加上Loading作为未请求到数据之前的过渡展示。由于多页面未使用vuex做状态管理,只在入口统一注册了bus,所以此例子使用eventbus做事件通信。

vue-cli3 实现多页面应用

公司有专门的审批系统,我现在做的后台管理系统需要接入,移动端和PC端都要有一个页面来展示业务信息。后端不给开俩项目(也确实没必要),所以打算用多页面来解决,PC和移动端放到一个项目里,然后打包到各自的文件夹

Webpack5 搭建React多页面应用

react-multi-page-app 是一个基于 webpack5 搭建的 react 多页面应用。为什么搭建多页面应用:多个页面之间业务互不关联,页面之间并没有共享的数据

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!