• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

webpack静态打包

武飞扬头像
liRemons
帮助1

举例

目录结构

│  .babelrc
│  .eslintrc
│  .gitignore
│  .prettierrc.json
│  LICENSE
│  package.json
│  postcss.config.js
│  tsconfig.json
│  webpack.config.js
├─config
│      cdn.js
│      rules.js
├─dist
├─logs
├─scripts
│      build.js
│      common.js
│      dev.js
│      log.js
│      pages.json
└─src
    │  index.html
    ├─apps
    	├─demo
    	├─demo2
    ├─assets
    ├─axios
    ├─components
    └─utils
学新通

关于 store,使用的是 royjs

业务代码

  • demo/app.jsx

    import { Button, Form, Card } from 'antd';
    import FormItem from '@components/Form';
    import { inject } from '@royjs/core';
    import store from './model/store'
    @inject(store)
    export default class App extends React.Component {
      render() {
        const { count } = this.props.state;
        return <Card>
          <Form
            labelCol={{ span: 4 }}
            wrapperCol={{ span: 20 }}
          >
            <FormItem label='数字输入' component='inputNumber'></FormItem>
            <FormItem label='输入' component='input'></FormItem>
            <FormItem label='选择器' component='select'></FormItem>
            <FormItem label='多选' component='checkbox'></FormItem>
            <Form.Item>
              <Button onClick={() => this.props.dispatch('add')}>   </Button>
            </Form.Item>
          </Form>
        </Card>
      }
    }
    
    
    学新通
  • demo/main.jsx

    import App from './app';
    ReactDOM.render(<App />, document.getElementById('container'));
    
  • demo/model/store.js

    import { Store } from '@royjs/core';
    
    export default new Store({
      state : {
        count: 0
      },
      actions:{
        add(state, payload){
          state.count   
        },
        reduce(state, payload){
          state.count --
        }
      }
    })
    
对常见的组件进行封装

Form 为例

  • components/Form/const.js

    import {
      Input,
      InputNumber,
      DatePicker,
      Select,
      Radio,
      Checkbox,
      Rate,
      Slider,
      TimePicker,
      Upload,
      TreeSelect,
      Cascader,
      Transfer
    } from "antd";
    const { RangePicker } = DatePicker;
    const { TextArea } = Input;
    export const Com = {
      'input': Input,
      'inputPassword': Input.Password,
      'textArea': TextArea,
      'inputNumber': InputNumber,
      'datePicker': DatePicker,
      'rangePicker': RangePicker,
      'select': Select,
      'radio': Radio,
      'radioGroup': Radio.Group,
      'checkbox': Checkbox,
      'rate': Rate,
      'slider': Slider,
      'timePicker': TimePicker,
      'upload': Upload,
      'treeSelect': TreeSelect,
      'cascader': Cascader,
      'transfer': Transfer
    }
    
    
    学新通
  • components/Form/index.jsx

    import React, { Component } from 'react'
    import { Form } from 'antd';
    import { Com } from './const'
    export default class FormItem extends Component {
      render() {
        const { label, name, component, required, ...rest } = this.props;
        const ReCompont = Com[component];
        const formProps = {
          label, name, component, required
        }
        return (
          <Form.Item {...formProps} rules={required ? [{ required: true }] : []} >
            <ReCompont style={{ minWidth: '100px' }} {...rest}></ReCompont>
          </Form.Item>
        )
      }
    }
    
    
    学新通
  • index.html 资源使用CDN引入,例:

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title><%= title %></title>
        <link rel="icon" href="favicon.ico" />
        <% for (const i in externals_css) {%>
        <link rel="stylesheet" href="<%= externals_css[i] %>" />
        <%} %>
      </head>
    
      <body>
        <div id="container"></div>
        <% for (const i in externals_js) {%>
        <script crossorigin src="<%= externals_js[i] %>"></script>
        <%} %>
      </body>
    </html>
    
    
    学新通

webpack 基本配置

以下为抽取静态html为例

webpack.config.js
const { Configuration, DefinePlugin, ProgressPlugin } = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const path = require('path')
const rules = require('./config/rules')
const pagesJSON = require('./scripts/pages.json')
const { js, css } = require('./config/cdn')
const packageJSON = require('./package.json')
/**
 * @type {Configuration}
 */

module.exports = (env, args) => {
  const mode = args.mode
  const pages = env.pages.split(',')
  const srcPagesDir = path.resolve(__dirname, 'src/apps/')
  const entry = {}
  const otherParams = {}
  ;(env.otherParams || '').split(',').forEach((item) => {
    otherParams[item.split('=')[0]] = item.split('=')[1]
  })
  console.log('正在编译以下应用', pages)
  pages.forEach((el) => (entry[el] = path.resolve(srcPagesDir, el, 'main.jsx')))
  const config = {
    entry,
    mode,
    output: {
      filename: (pathData) => {
        return pages.includes(pathData.runtime)
          ? '[name]/js/[contenthash:10].js'
          : '[name].js'
      },
      path: path.resolve(__dirname, `dist/@${packageJSON.name}`),
      publicPath: `/@${packageJSON.name}/`,
    },
    optimization: {
      minimize: true,
      minimizer: [
        new TerserPlugin({
          minify: (file, sourceMap) => {
            const uglifyJsOptions = {
              sourceMap: false,
            }
            return require('uglify-js').minify(file, uglifyJsOptions)
          },
        }),
      ],
      splitChunks: {
        chunks: 'async',
        minSize: 20000,
        minRemainingSize: 0,
        minChunks: 1,
        maxAsyncRequests: 30,
        maxInitialRequests: 30,
        enforceSizeThreshold: 50000,
        cacheGroups: {
          defaultVendors: {
            test: /[\\/]node_modules[\\/]/,
            priority: -10,
            reuseExistingChunk: true,
          },
          default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
          },
        },
      },
    },
    module: {
      rules,
    },
    resolve: {
      extensions: ['.js', '.jsx', '.tsx', '.ts'],
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '@components': path.resolve(__dirname, 'src/components'),
        '@axios': path.resolve(__dirname, 'src/axios'),
        '@assets': path.resolve(__dirname, 'src/assets'),
        '@utils': path.resolve(__dirname, 'src/utils'),
      },
    },
    externals: {
      react: 'React',
      'react-dom': 'ReactDOM',
      antd: 'antd',
      mobx: 'mobx',
      'mobx-react': 'mobxReact',
      classnames: 'classNames',
      axios: 'axios',
      qs: 'Qs',
    },
    plugins: [
      ...pages.map((pageName) => {
        const pageInfo =
          pagesJSON.find((item) => item.pageName === pageName) || {}
        return new HtmlWebpackPlugin({
          filename: `${pageName}/index.html`,
          chunks: [pageName],
          template: path.resolve(__dirname, 'src/index.html'),
          templateParameters: (compilation, assets, assetTags, options) => {
            const externals_js = []
            const externals_css = []
            const externalsValues = []
            for (let [key, value] of compilation._modules.entries()) {
              if (key.includes('external')) {
                externalsValues.push(value.userRequest)
              }
            }

            js.forEach((item) => {
              externalsValues.forEach((val) => {
                if (item.externalsName === val) {
                  externals_js.push(item.url)
                }
              })
            })
            css.forEach((item) => {
              externalsValues.forEach((val) => {
                if (item.externalsName === val) {
                  externals_css.push(item.url)
                }
              })
            })
            if (mode === 'production') {
              console.log('---------------------------------')
              console.log(`正在写入模板 页面:${pageName}/index.html:  cdn/js`)
              console.log(externals_js)
              console.log(`正在写入模板 页面:${pageName}/index.html:  cdn/css`)
              console.log(externals_css)
              console.log('---------------------------------')
            }

            return {
              title: `remons.cn - ${pageInfo.title}`,
              externals_js: [...new Set(externals_js)],
              externals_css: [...new Set(externals_css)],
            }
          },
        })
      }),
      new ProgressPlugin({
        activeModules: true,
        modules: true,
      }),
      // 提取单独的CSS
      new MiniCssExtractPlugin({
        filename: '[name]/main.[contenthash:10].css',
      }),
      new DefinePlugin({
        APP_NAME: JSON.stringify(`@${packageJSON.name}`),
      }),
      // 压缩css
      new CssMinimizerPlugin(),
      new BundleAnalyzerPlugin({
        defaultSizes: 'stat',
        analyzerMode:
          mode === 'production' && otherParams.report === 'true'
            ? 'server'
            : 'disabled',
      }),
      mode === 'production' && otherParams.gzip === 'true'
        ? new CompressionPlugin()
        : null,
    ].filter((_) => !!_),
    devServer: {
      static: {
        directory: path.resolve(__dirname, 'dist'),
      },
      compress: true,
      port: 3033,
      host: 'local-ip',
      open: [`/@${packageJSON.name}/${env.pages.split(',')[0]}`],
      hot: true,
      client: {
        progress: true,
      },
    },
    stats: 'errors-only',
    devtool: mode === 'development' ? 'eval-source-map' : false,
  }
  return config
}

学新通
config/rules.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const path = require('path')
// test: the applied rule.
// exclude: ignore processed files.
// use: multiple loaders can be used to run from bottom to top.

// Handle compatibility
const postcssLoader = {
  loader: 'postcss-loader',
}

const cssMoudleLoader = {
  loader: 'css-loader',
  options: {
    sourceMap: false,
    modules: {
      localIdentName: '[path][name]-[local]-[hash:base64:10]',
    },
  },
}

const rules = [
  {
    oneOf: [
      // css
      {
        test: /\.css$/,
        include: path.resolve(__dirname, '../src'),
        // MiniCssExtractPlugin.loader plug-in extracts css as a separate file.
        // Unlike style-loader, style-loader inserts css into the style tag.
        use: [MiniCssExtractPlugin.loader, 'css-loader', postcssLoader],
      },
      // less
      {
        test: new RegExp(`^(?!.*\\.global).*\\.less`),
        include: path.resolve(__dirname, '../src'),
        include: path.resolve(__dirname, '../src'),
        use: [
          MiniCssExtractPlugin.loader,
          cssMoudleLoader,
          postcssLoader,
          'less-loader',
        ],
      },
      {
        test: new RegExp(`^(.*\\.global).*\\.less`),
        include: path.resolve(__dirname, '../src'),
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          postcssLoader,
          'less-loader',
        ],
      },
      // Process images.
      {
        test: /\.(jpg|png|jpeg|gif)$/,
        include: path.resolve(__dirname, '../src'),
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 1024 * 8,
              name: '[name].[ext]',
              esModule: false,
              outputPath: 'static/assets/images',
            },
          },
        ],
      },
      // Static resources in HTML
      {
        test: /\.html$ /,
        include: path.resolve(__dirname, '../src'),
        loader: 'html-loader',
        options: {
          esModule: false,
        },
      },
      {
        test: /\.js|jsx$/,
        include: path.resolve(__dirname, '../src'),
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /\.ts|tsx$/,
        include: path.resolve(__dirname, '../src'),
        use: {
          loader: 'ts-loader',
        },
      },
      // Other resources
      {
        test: /\.(pdf|doc|node|svg)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              outputPath: (url, resourcePath) => {
                console.log(resourcePath);
                return `${
                  (resourcePath || '')
                    .replace(/\//g, '_')
                    .replace(/\\/g, '_')
                    .split('apps')[1]
                    .split('_')
                    .filter((_) => !!_)[0]
                }/assets/file/${url}`
              },
            },
          },
        ],
      },
    ],
  },
]

module.exports = rules

学新通
config/cdn.js
const js = [
  { externalsName: 'react', url: "https://unpkg.com/react@17.0.2/umd/react.production.min.js" },
  { externalsName: 'react-dom', url: "https://unpkg.com/react-dom@17.0.2/umd/react-dom.production.min.js" },
  { externalsName: 'qs', url: "https://unpkg.com/qs@6.10.3/dist/qs.js" },
  { externalsName: 'axios', url: "https://unpkg.com/axios/dist/axios.min.js" },
  { externalsName: 'classnames', url: "https://unpkg.com/classnames@2.3.1/index.js" },
  { externalsName: 'mobx', url: "https://unpkg.com/mobx@6.3.2/dist/mobx.umd.production.min.js" },
  { externalsName: 'mobx-react', url: "https://unpkg.com/mobx-react-lite@3.1.6/dist/mobxreactlite.umd.production.min.js" },
  { externalsName: 'mobx-react', url: "https://unpkg.com/mobx-react@7.3.0/dist/mobxreact.umd.production.min.js" },
  { externalsName: 'moment', url: "https://unpkg.com/moment@2.29.1/min/moment.min.js" },
  { externalsName: 'antd', url: "https://unpkg.com/moment@2.29.1/min/moment.min.js" },
  { externalsName: 'antd', url: "https://unpkg.com/antd@4.18.9/dist/antd.min.js" },
];

const css = [
  { externalsName: 'antd', url: "https://unpkg.com/antd@4.18.9/dist/antd.min.css", },
]

module.exports = {
  js, css
}
学新通
postcss.config.js
module.exports = {
  plugins: {
    autoprefixer: {
      overrideBrowserslist: [
        "ie >= 8", // 兼容IE7以上浏览器
        "Firefox >= 3.5", // 兼容火狐版本号大于3.5浏览器
        "chrome  >= 35", // 兼容谷歌版本号大于35浏览器,
        "opera >= 11.5", // 兼容欧朋版本号大于11.5浏览器
      ],
    },
  },
};

执行打包命令

执行命令的配置文件
scripts/common.js
const fsExtra = require('fs-extra')
const { readdirSync } = require('fs')
const fs = require('fs')
const log4js = require('log4js')
const chalk = require('chalk')
const { logCongfig } = require('./log')
const pakeageJSON = require('../package.json')
log4js.configure(logCongfig)

const deletePath = (pages) =>
  Promise.all(
    pages.map((item) => fsExtra.remove(`dist/@${pakeageJSON.name}/${item}`))
  )

const getPages = () => {
  return new Promise(async (resolve, reject) => {
    const pages =
      ((process.argv[2] || '').includes('=') ? '' : process.argv[2]) ||
      readdirSync('src/apps').join(',')
    if (!pages) {
      reject('未找到目录')
      return
    }

    const set1 = new Set(pages.split(','))
    const set2 = new Set(readdirSync('src/apps'))
    let difference = new Set([...set1].filter((x) => !set2.has(x)))
    if (difference.size) {
      console.error('识别到以下目录不存在,请检查:')
      reject([...difference].join(','))
      return
    }
    console.log('识别到以下目录:')
    const otherParams =
      process.argv.findIndex((i) => (i || '').includes('=')) !== -1
        ? process.argv.slice(
            process.argv.findIndex((i) => i.includes('=')),
            process.argv.length
          )
        : []
    pages.split(',').forEach((el, index) => console.log(`${index   1}:`, el))
    await deletePath(pages.split(','))
    resolve({ pages, otherParams })
  })
}

const getDist = (pages) => {
  let filesDir = []
  const readDirSize = (path) => {
    fs.readdir(path, {}, (err, files) => {
      if (files) {
        files.forEach((item) => {
          readDirSize(`${path}/${item}`)
        })
      } else {
        fs.stat(path, (err, stats) => {
          filesDir.push(
            `${path.replace('dist', '')}__size__${(stats.size / 1024).toFixed(
              2
            )}`
          )
        })
      }
    })
  }

  readDirSize('dist')
  setTimeout(() => {
    const size = filesDir
      .map((item) =>  item.split('__size__')[1])
      .reduce((a, b) => a   b)
      .toFixed(2)
    console.log(
      `dist: 合计大小 ${size > 1024 ? (size / 1024).toFixed(2) : size} ${
        size > 1024 ? 'mb' : 'kb'
      }, 资源如下:`
    )
    const newFileArr = pages.map((page) => {
      return {
        page,
        files: filesDir
          .sort()
          .map((item) => {
            if (
              item
                .replace(/\//g, '_')
                .replace(/\\/g, '_')
                .split('_')
                .filter((_) => !!_)
                .includes(page)
            ) {
              return item
            }
          })
          .filter((_) => !!_),
      }
    })
    newFileArr.forEach((item) => {
      console.log(chalk.green(' '))
      const filesSize = item.files
        .map((item) =>  item.split('__size__')[1])
        .reduce((a, b) => a   b)
        .toFixed(2)
      console.log(
        chalk.bgGreen(
          chalk.black(
            ` ${item.page}:`,
            `${filesSize > 1024 ? (filesSize / 1024).toFixed(2) : filesSize} ${
              filesSize > 1024 ? 'mb' : 'kb'
            } `
          )
        )
      )
      item.files.forEach((el) => {
        console.log(
          chalk.green(el.split('__size__')[0]),
          chalk.yellow(`${el.split('__size__')[1]} kb`)
        )
      })
    })
    console.log(
      '如需查看打包详细依赖和大小,请重新执行命令: npm run build (your apps) report=true 或 npm run build report=true'
    )
  }, 100)
}

module.exports = {
  getPages,
  getDist,
}

学新通
scripts/build.js
const { execSync } = require("child_process");
const { getPages, getDist } = require('./common');
getPages().then(({ pages, otherParams }) => {
  if ((otherParams || '').includes('report=true')) {
    const command = `webpack --mode=production --env pages=${pages} otherParams=${otherParams}`;
    execSync(command, { stdio: "inherit" });
    getDist();
  } else {
    pages.split(',').forEach((page, index) => {
      const command = `webpack --mode=production --env pages=${page} otherParams=${otherParams}`;
      execSync(command, { stdio: "inherit" });
      console.log([page], '打包完成', `共 ${pages.split(',').length} 个应用,当前已打包 ${index   1} 个`);
      if (index === pages.split(',').length - 1) {
        console.log('========所有应用已打包完成========');
        getDist(pages.split(','));
      }
    })
  }
})


学新通
scripts/dev.js
const { execSync } = require("child_process");
const { getPages } = require('./common');
getPages().then(({ pages, otherParams }) => {
  const command = `webpack serve  --mode=development --env pages=${pages} otherParams=${otherParams}`;
  execSync(command, { stdio: "inherit" });
});

scripts/log.js

略(log4js)

scripts/pages.json
// 用来生成 META TITLE
[
  {
    "pageName": "demo",
    "title": "demo 测试"
  },
]

package.json
{
  "name": "website_pages",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node scripts/dev.js",
    "dev": "npm start",
    "build": "node scripts/build.js"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.14.6",
    "@babel/plugin-proposal-decorators": "^7.14.5",
    "@babel/plugin-transform-runtime": "^7.14.5",
    "@babel/preset-env": "^7.14.7",
    "@babel/preset-react": "^7.14.5",
    "@types/react": "^17.0.39",
    "@types/react-dom": "^17.0.12",
    "babel-loader": "^8.2.2",
    "chalk": "^4.1.2",
    "compression-webpack-plugin": "^8.0.1",
    "css-loader": "^5.2.6",
    "css-minimizer-webpack-plugin": "^3.0.2",
    "extract-loader": "^5.1.0",
    "file-loader": "^6.2.0",
    "fs-extra": "^10.0.1",
    "html-loader": "^2.1.2",
    "html-webpack-plugin": "^5.3.2",
    "husky": "^7.0.4",
    "less": "^4.1.1",
    "less-loader": "^10.0.1",
    "log4js": "^6.4.2",
    "mini-css-extract-plugin": "^2.1.0",
    "postcss": "^8.3.5",
    "postcss-loader": "^6.1.1",
    "postcss-preset-env": "^6.7.0",
    "prettier": "^2.5.0",
    "style-loader": "^3.0.0",
    "ts-loader": "^9.2.7",
    "tsconfig-paths-webpack-plugin": "^3.5.2",
    "typescript": "^4.5.5",
    "url-loader": "^4.1.1",
    "webpack": "^5.44.0",
    "webpack-bundle-analyzer": "^4.5.0",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^4.9.0",
    "uglify-js": "^3.15.5"
  },
  "dependencies": {
    "antd": "^4.16.8",
    "axios": "^0.24.0",
    "classnames": "^2.3.1",
    "crypto-js": "^4.1.1",
    "methods-r": "^1.2.14",
    "mobx": "^6.3.5",
    "mobx-react": "^7.2.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

学新通
打包命令
  • dev : npm run dev 文件夹名称 例:npm run dev demo1,demo2
  • build: npm run build 文件夹名称 例:npm run build demo1,demo2
  • 注意:默认会查找 src/pages 目录下的文件夹,最终打包产物会在根目录 dist

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhghegbf
系列文章
更多 icon
同类精品
更多 icon
继续加载