16、Webpack 4 实战:样式处理、性能优化与服务端渲染

Webpack 4 实战:样式处理、性能优化与服务端渲染

1. 样式处理

1.1 样式渲染原理

当我们检查页面时,会发现动态注入了一个带有临时 URL 的 <link> 标签,其中包含编译后的 CSS。类名呈现为 “Home_Home_2kP…” 这种形式,这是因为配置了 localIdentName: '[name]_[local]_[hash:base64]' ,这样可以创建隔离的样式,避免使用相同类名时相互影响。

1.2 实现 CSS 预处理器

若要将 CSS 代码提取到 style.css 文件并在生产模式下压缩代码,可按以下步骤操作:
1. 安装插件

npm install extract-text-webpack-plugin@v4.0.0-beta.0
  1. 添加到 Webpack 插件
import HtmlWebPackPlugin from 'html-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [
  new HtmlWebPackPlugin({
    title: 'Codejobs',
    template: './src/index.html',
    filename: './index.html'
  })
];
if (isProduction) {
  plugins.push(
    new ExtractTextPlugin({
      allChunks: true,
      filename: './css/[name].css'
    })
  );
}
export default plugins;
  1. package.json 中添加脚本
"scripts": {
  "start": "webpack-dev-server --mode development --open",
  "start-production": "NODE_ENV=production webpack-dev-server --mode production",
  "build-development": "webpack --mode development",
  "build": "webpack --mode production"
}
  1. 在终端运行生产模式
npm run start-production
  1. 添加规则到 module 节点
import ExtractTextPlugin from 'extract-text-webpack-plugin';
const isProduction = process.env.NODE_ENV === 'production';
const rules = [
  {
    test: /\.(js|jsx)$/,
    exclude: /node_modules/,
    use: 'babel-loader'
  }
];
if (isProduction) {
  rules.push({
    test: /\.scss/,
    use: ExtractTextPlugin.extract({
      fallback: 'style-loader',
      use: [
        'css-loader?minimize=true&modules=true&localIdentName=[name]_[local]_[hash:base64]',
        'sass-loader'
      ]
    })
  });
} else {
  rules.push({
    test: /\.scss$/, // .scss - .styl - .less
    use: [
      {
        loader: 'style-loader'
      },
      {
        loader: 'css-loader',
        options: {
          modules: true,
          importLoaders: 1,
          localIdentName: '[name]_[local]_[hash:base64]',
          sourceMap: true,
          minimize: true
        }
      },
      {
        loader: 'sass-loader' // sass-loader, stylus-loader or less-loader
      }
    ]
  });
}
export default {
  rules
};

2. Webpack 4 优化 - 拆分捆绑包

2.1 准备工作

安装以下包:

npm install webpack-bundle-analyzer webpack-notifier

2.2 操作步骤

  1. 添加源映射
    创建 webpack/configuration/devtool.js 文件:
const isProduction = process.env.NODE_ENV === 'production';
export default !isProduction ? 'cheap-module-source-map' : 'eval';
  1. 拆分捆绑包
    创建 optimization.js 文件:
export default {
  splitChunks: {
    cacheGroups: {
      default: false,
      commons: {
        test: /node_modules/,
        name: 'vendor',
        chunks: 'all'
      }
    }
  }
}
  1. 将新文件导入 index.js
// Configuration
import devtool from './devtool';
import module from './module';
import optimization from './optimization';
import plugins from './plugins';
import resolve from './resolve';
export {
  devtool,
  module,
  optimization,
  plugins,
  resolve
};
  1. 将节点添加到 webpack.config.babel.js
import {
  devtool,
  module,
  optimization,
  plugins,
  resolve
} from './webpack/configuration';
export default {
  devtool,
  module,
  plugins,
  optimization,
  resolve
};

2.3 测试效果

  • 运行 npm start ,查看 HTML 会自动注入 vendor.js main.js 捆绑包。
  • 运行 npm run start-production ,会发现捆绑包变小,优化后捆绑包大小减少 40%。

2.4 使用插件

  • BundleAnalyzerPlugin :可查看所有包和组件的大小。
  • WebpackNotifierPlugin :每次 Webpack 构建时显示通知。
import HtmlWebPackPlugin from 'html-webpack-plugin';
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import WebpackNotifierPlugin from 'webpack-notifier';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [
  new HtmlWebPackPlugin({
    title: 'Codejobs',
    template: './src/index.html',
    filename: './index.html'
  })
];
if (isProduction) {
  plugins.push(
    new ExtractTextPlugin({
      allChunks: true,
      filename: './css/[name].css'
    })
  );
} else {
  plugins.push(
    new BundleAnalyzerPlugin(),
    new WebpackNotifierPlugin({
      title: 'CodeJobs'
    })
  );
}
export default plugins;

3. 结合 Node.js、React/Redux 和 Webpack 4

3.1 准备工作

安装以下包:

npm install babel-cli express nodemon react-hot-loader react-router-dom webpack-hot-middleware compression-webpack-plugin react-redux redux

3.2 实现步骤

  1. .babelrc 中添加 react-hot-loader 插件
{
  "presets": ["env", "react"],
  "env": {
    "development": {
      "plugins": [
        "react-hot-loader/babel"
      ]
    }
  }
}
  1. 创建 Express 服务器
    创建 src/server/index.js 文件:
// Dependencies
import express from 'express';
import path from 'path';
import webpackDevMiddleware from 'webpack-dev-middleware';
import webpackHotMiddleware from 'webpack-hot-middleware';
import webpack from 'webpack';
// Webpack Configuration
import webpackConfig from '../../webpack.config.babel';
// Client Render
import clientRender from './render/clientRender';
// Utils
import { isMobile } from '../shared/utils/device';
// Environment
const isProduction = process.env.NODE_ENV === 'production';
// Express Application
const app = express();
// Webpack Compiler
const compiler = webpack(webpackConfig);

// Webpack Middleware
if (!isProduction) {
  // Hot Module Replacement
  app.use(webpackDevMiddleware(compiler));
  app.use(webpackHotMiddleware(compiler));
} else {
  // Public directory
  app.use(express.static(path.join(__dirname, '../../public')));
  // GZip Compression just for Production
  app.get('*.js', (req, res, next) => {
    req.url = `${req.url}.gz`;
    res.set('Content-Encoding', 'gzip');
    next();
  });
}
// Device Detection
app.use((req, res, next) => {
  req.isMobile = isMobile(req.headers['user-agent']);
  next();
});
// Client Side Rendering
app.use(clientRender());
// Disabling x-powered-by
app.disable('x-powered-by');
// Listen Port 3000...
app.listen(3000);
  1. 创建设备检测工具文件
export function getCurrentDevice(ua) {
  return /mobile/i.test(ua) ? 'mobile' : 'desktop';
}
export function isDesktop(ua) {
  return !/mobile/i.test(ua);
}
export function isMobile(ua) {
  return /mobile/i.test(ua);
}
  1. 创建设备 reducer
export default function deviceReducer(state = {}) {
  return state;
}
  1. 合并 reducers
// Dependencies
import { combineReducers } from 'redux';
// Shared Reducers
import device from './deviceReducer';
const rootReducer = combineReducers({
  device
});
export default rootReducer;
  1. 创建初始状态文件
export default req => ({
  device: {
    isMobile: req.isMobile
  }
});
  1. 配置 Redux 存储
// Dependencies
import { createStore } from 'redux';
// Root Reducer
import rootReducer from '../reducers';
export default function configureStore(initialState) {
  return createStore(
    rootReducer,
    initialState
  );
}
  1. 创建 HTML 渲染文件
// Dependencies
import serialize from 'serialize-javascript';
// Environment
const isProduction = process.env.NODE_ENV === 'production';
export default function html(options) {
  const { title, initialState } = options;
  let path = '/';
  let link = '';
  if (isProduction) {
    path = '/app/';
    link = `<link rel="stylesheet" href="${path}css/main.css" />`;
  }
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>${title}</title>
        ${link}
      </head>
      <body>
        <div id="root"></div>
        <script>
          window.initialState = ${serialize(initialState)};
        </script>
        <script src="${path}vendor.js"></script>
        <script src="${path}main.js"></script>
      </body>
    </html>
  `;
}
  1. 创建 HTML 渲染函数
// HTML
import html from './html';
// Initial State
import initialState from './initialState';
export default function clientRender() {
  return (req, res) => res.send(html({
    title: 'Codejobs',
    initialState: initialState(req)
  }));
}
  1. 创建客户端主入口文件
// Dependencies
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { AppContainer } from 'react-hot-loader';
// Redux Store
import configureStore from './shared/redux/configureStore';
// Components
import App from './client/App';
// Configuring Redux Store
const store = configureStore(window.initialState);
// Root element
const rootElement = document.querySelector('#root');
// App Wrapper
const renderApp = Component => {
  render(
    <AppContainer>
      <Provider store={store}>
        <Component />
      </Provider>
    </AppContainer>,
    rootElement
  );
};
// Rendering app
renderApp(App);
// Hot Module Replacement
if (module.hot) {
  module.hot.accept('./client/App', () => {
    renderApp(require('./client/App').default);
  });
}
  1. 创建客户端路由文件
// Dependencies
import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
// Components
import About from './components/About';
import Home from './components/Home';
const App = () => (
  <BrowserRouter>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route exact path="/about" component={About} />
    </Switch>
  </BrowserRouter>
);
export default App;
  1. 创建 About 组件
import React from 'react';
import { bool } from 'prop-types';
import { connect } from 'react-redux';
import styles from './About.scss';
const About = ({ isMobile }) => (
  <h1 className={styles.About}>About - {isMobile ? 'mobile' : 'desktop'}</h1>
);
About.propTypes = {
  isMobile: bool
};
export default connect(({ device }) => ({
  isMobile: device.isMobile
}))(About);
  1. 添加 About 组件样式
$color: green;
.About {
  color: $color;
}
  1. 添加 React Hot Loader 入口
const isProduction = process.env.NODE_ENV === 'production';
const entry = [];
if (!isProduction) {
  entry.push(
    'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr&reload=true',
    'react-hot-loader/patch',
    './src/index.jsx'
  );
} else {
  entry.push('./src/index.jsx');
}
export default entry;
  1. 创建输出文件配置
// Dependencies
import path from 'path';
export default {
  filename: '[name].js',
  path: path.resolve(__dirname, '../../public/app'),
  publicPath: '/'
};
  1. 将文件导入 index.js
// Configuration
import devtool from './devtool';
import entry from './entry';
import mode from './mode';
import module from './module';
import optimization from './optimization';
import output from './output';
import plugins from './plugins';
import resolve from './resolve';
export {
  devtool,
  entry,
  mode,
  module,
  optimization,
  output,
  plugins,
  resolve
};
  1. 创建模式配置文件
const isProduction = process.env.NODE_ENV === 'production';
export default !isProduction ? 'development' : 'production';
  1. 添加插件到 plugins.js
import ExtractTextPlugin from 'extract-text-webpack-plugin';
import WebpackNotifierPlugin from 'webpack-notifier';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import CompressionPlugin from 'compression-webpack-plugin';
import webpack from 'webpack';
const isProduction = process.env.NODE_ENV === 'production';
const plugins = [];
if (isProduction) {
  plugins.push(
    new ExtractTextPlugin({
      allChunks: true,
      filename: './css/[name].css'
    }),
    new CompressionPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: /\.js$/,
      threshold: 10240,
      minRatio: 0.8
    })
  );
} else {
  plugins.push(
    new webpack.HotModuleReplacementPlugin(),
    new BundleAnalyzerPlugin(),
    new WebpackNotifierPlugin({
      title: 'CodeJobs'
    })
  );
}
export default plugins;
  1. 更新 package.json 脚本
"scripts": {
  "build": "NODE_ENV=production webpack",
  "clean": "rm -rf public/app",
  "start": "npm run clean && NODE_ENV=development nodemon src/server --watch src/server --exec babel-node --presets es2015",
  "start-production": "npm run clean && npm run build && NODE_ENV=production babel-node src/server --presets es2015"
}

3.3 运行效果

  • 运行 npm start ,可看到页面,打开浏览器控制台会发现 HMR 已连接,修改 Home 组件内容可实现无刷新更新。
  • 运行 npm run start-production ,可在生产模式下运行应用,捆绑包会变小。

3.4 使用别名简化导入路径

  1. 安装 babel-plugin-module-resolver
npm install babel-plugin-module-resolver
  1. .babelrc 中添加别名
{
  "presets": ["env", "react"],
  "env": {
    "development": {
      "plugins": [
        "react-hot-loader/babel"
      ]
    }
  },
  "plugins": [
    ["module-resolver", {
      "root": ["./"],
      "alias": {
        "@App": "./src/client/App.jsx",
        "@client": "./src/client/",
        "@components": "./src/client/components",
        "@configureStore": "./src/shared/redux/configureStore.js",
        "@reducers": "./src/shared/reducers",
        "@server": "./src/server/",
        "@utils": "./src/shared/utils",
        "@webpack": "./webpack.config.babel.js"
      }
    }]
  ]
}

流程图

graph TD;
    A[开始] --> B[安装依赖];
    B --> C[配置 Webpack];
    C --> D[创建服务器];
    D --> E[配置 Redux];
    E --> F[创建组件和路由];
    F --> G[添加样式和插件];
    G --> H[运行开发或生产模式];
    H --> I[测试应用];

总结

通过以上步骤,我们实现了 Webpack 4 的样式处理、性能优化以及结合 Node.js、React/Redux 的应用开发。掌握这些技巧可以帮助我们构建更高效、更强大的 Web 应用。在实际开发中,可根据项目需求灵活调整配置和代码。

4. 实现服务器端渲染

4.1 服务器端渲染原理

传统的 React 通常使用客户端渲染(CSR),即动态地将 HTML 代码注入到目标 div 中。而服务器端渲染(SSR)则是在服务器端生成完整的 HTML 页面,然后将其发送到客户端。这样做的好处包括更好的搜索引擎优化(SEO)、更快的首屏加载时间等。

4.2 实现步骤

  1. 安装必要的依赖
    - 确保已经安装了 express react react - dom react - redux redux 等相关依赖。
  2. 创建服务器端渲染逻辑
    - 在服务器端代码中,使用 ReactDOMServer.renderToString 方法将 React 组件渲染为字符串。
    - 示例代码如下:
import React from'react';
import { renderToString } from'react - dom/server';
import { Provider } from'react - redux';
import { createStore } from'redux';
import rootReducer from './reducers';
import App from './client/App';

// 创建 Redux 存储
const store = createStore(rootReducer);

// 渲染 React 组件为字符串
const html = renderToString(
  <Provider store={store}>
    <App />
  </Provider>
);

// 返回包含渲染结果的 HTML 页面
const fullHtml = `
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf - 8">
      <title>Server - Side Rendered App</title>
    </head>
    <body>
      <div id="root">${html}</div>
      <script>
        window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())};
      </script>
      <script src="/bundle.js"></script>
    </body>
  </html>
`;
  1. 在 Express 服务器中使用服务器端渲染
    - 在 Express 服务器代码中,处理客户端请求并返回服务器端渲染的 HTML 页面。
    - 示例代码如下:
import express from 'express';
import { getFullHtml } from './render'; // 假设 getFullHtml 是上面生成 fullHtml 的函数

const app = express();

app.get('/', (req, res) => {
  const html = getFullHtml();
  res.send(html);
});

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});
  1. 在客户端激活 React 应用
    - 在客户端代码中,使用 ReactDOM.hydrate 方法将服务器端渲染的 HTML 激活为 React 应用。
    - 示例代码如下:
import React from'react';
import { hydrate } from'react - dom';
import { Provider } from'react - redux';
import { createStore } from'redux';
import rootReducer from './reducers';
import App from './client/App';

// 获取服务器端传递的初始状态
const initialState = window.__INITIAL_STATE__;

// 创建 Redux 存储
const store = createStore(rootReducer, initialState);

// 激活 React 应用
hydrate(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

4.3 服务器端渲染的优势和挑战

优势 挑战
更好的 SEO,搜索引擎可以直接抓取完整的 HTML 页面 服务器端负载增加,需要更多的服务器资源
更快的首屏加载时间,用户可以更快地看到页面内容 开发和调试复杂度增加,需要处理服务器端和客户端的不同环境
支持不支持 JavaScript 的设备 需要处理服务器端和客户端状态同步的问题

5. 实现带有服务器端渲染的 Promise

5.1 需求背景

在服务器端渲染中,有时候组件可能依赖于异步数据,例如从 API 获取数据。这就需要处理 Promise,确保在数据获取完成后再进行渲染。

5.2 实现步骤

  1. 在组件中添加异步数据获取逻辑
    - 例如,在一个 UserList 组件中,从 API 获取用户列表。
import React, { useEffect, useState } from'react';

const UserList = () => {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    const fetchUsers = async () => {
      const response = await fetch('https://api.example.com/users');
      const data = await response.json();
      setUsers(data);
    };
    fetchUsers();
  }, []);

  return (
    <div>
      <h2>User List</h2>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;
  1. 在服务器端等待 Promise 完成
    - 在服务器端渲染时,需要等待组件中的 Promise 完成后再进行渲染。
    - 可以使用 Promise.all 来等待多个 Promise。
import React from'react';
import { renderToString } from'react - dom/server';
import { Provider } from'react - redux';
import { createStore } from'redux';
import rootReducer from './reducers';
import App from './client/App';

// 假设 App 组件包含需要等待的异步操作
const fetchData = () => {
  // 这里可以处理组件中的异步操作
  return Promise.resolve();
};

const renderApp = async () => {
  await fetchData();
  const store = createStore(rootReducer);
  const html = renderToString(
    <Provider store={store}>
      <App />
    </Provider>
  );
  return html;
};

const getFullHtml = async () => {
  const appHtml = await renderApp();
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf - 8">
        <title>Server - Side Rendered App with Promise</title>
      </head>
      <body>
        <div id="root">${appHtml}</div>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())};
        </script>
        <script src="/bundle.js"></script>
      </body>
    </html>
  `;
};

export { getFullHtml };

6. 使用 Next.js 进行服务器端渲染

6.1 Next.js 简介

Next.js 是一个基于 React 的服务器端渲染框架,它简化了服务器端渲染的开发过程,提供了很多内置的功能,如自动代码分割、静态生成等。

6.2 快速开始

  1. 创建 Next.js 项目
    - 使用 npx create - next - app my - next - app 创建一个新的 Next.js 项目。
  2. 创建页面
    - 在 pages 目录下创建页面组件,Next.js 会自动将这些组件映射为路由。
    - 例如,创建 pages/index.js
import React from'react';

const HomePage = () => {
  return (
    <div>
      <h1>Welcome to Next.js</h1>
    </div>
  );
};

export default HomePage;
  1. 运行开发服务器
    - 在项目根目录下运行 npm run dev ,启动开发服务器。
    - 打开浏览器访问 http://localhost:3000 即可看到页面。

6.3 Next.js 的高级功能

  • 静态生成(SSG) :在构建时生成静态 HTML 页面,适合内容更新不频繁的页面。
  • 增量静态再生(ISR) :在构建时生成静态页面,并且可以在运行时更新这些页面。
  • API 路由 :在 Next.js 中可以轻松创建 API 路由,处理服务器端逻辑。

流程图

graph TD;
    A[开始服务器端渲染] --> B[安装依赖];
    B --> C[创建服务器端渲染逻辑];
    C --> D[处理异步数据(Promise)];
    D --> E[使用 Next.js 简化开发];
    E --> F[部署应用];
    F --> G[测试和优化];

总结

服务器端渲染是提升 Web 应用性能和 SEO 的重要手段。通过实现服务器端渲染、处理 Promise 以及使用 Next.js 等框架,我们可以构建出更高效、更友好的 Web 应用。在实际开发中,需要根据项目的需求和特点选择合适的技术和方案,同时要注意处理好服务器端和客户端的不同环境以及状态同步等问题。

基于径向基函数神经网络RBFNN的自适应滑模控制学习(Matlab代码实现)内容概要:本文介绍了基于径向基函数神经网络(RBFNN)的自适应滑模控制方法,并提供了相应的Matlab代码实现。该方法结合了RBF神经网络的非线性逼近能力和滑模控制的强鲁棒性,用于解决复杂系统的控制问题,尤其适用于存在不确定性和外部干扰的动态系统。文中详细阐述了控制算法的设计思路、RBFNN的结构权重更新机制、滑模面的构建以及自适应律的推导过程,并通过Matlab仿真验证了所提方法的有效性和稳定性。此外,文档还列举了大量相关的科研方向和技术应用,涵盖智能优化算法、机器学习、电力系统、路径规划等多个领域,展示了该技术的广泛应用前景。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的研究生、科研人员及工程技术人员,特别是从事智能控制、非线性系统控制及相关领域的研究人员; 使用场景及目标:①学习和掌握RBF神经网络滑模控制相结合的自适应控制策略设计方法;②应用于电机控制、机器人轨迹跟踪、电力电子系统等存在模型不确定性或外界扰动的实际控制系统中,提升控制精度鲁棒性; 阅读建议:建议读者结合提供的Matlab代码进行仿真实践,深入理解算法实现细节,同时可参考文中提及的相关技术方向拓展研究思路,注重理论分析仿真验证相结合。
先展示下效果 https://pan.quark.cn/s/a4b39357ea24 本项目是本人参加BAT等其他公司电话、现场面试之后总结出来的针对Java面试的知识点或真题,每个点或题目都是在面试中被问过的。 除开知识点,一定要准备好以下套路: 个人介绍,需要准备一个1分钟的介绍,包括学习经历、工作经历、项目经历、个人优势、一句话总结。 一定要自己背得滚瓜烂熟,张口就来 抽象概念,当面试官问你是如何理解多线程的时候,你要知道从定义、来源、实现、问题、优化、应用方面系统性地回答 项目强化,至少知识点的比例是五五开,所以必须针对简历中的两个以上的项目,形成包括【架构和实现细节】,【正常流程和异常流程的处理】,【难点+坑+复盘优化】三位一体的组合拳 压力练习,面试的时候难免紧张,可能会严重影响发挥,通过平时多找机会参交流分享,或找人做压力面试来改善 表达练习,表达能力非常影响在面试中的表现,能否简练地将答案告诉面试官,可以通过给自己讲解的方式刻意练习 重点针对,面试官会针对简历提问,所以请针对简历上写的所有技术点进行重点准备 Java基础 JVM原理 集合 多线程 IO 问题排查 Web框架、数据库 Spring MySQL Redis 通用基础 操作系统 网络通信协议 排序算法 常用设计模式 从URL到看到网页的过程 分布式 CAP理论 锁 事务 消息队列 协调器 ID生成方式 一致性hash 限流 微服务 微服务介绍 服务发现 API网关 服务容错保护 服务配置中心 算法 数组-快速排序-第k大个数 数组-对撞指针-最大蓄水 数组-滑动窗口-最小连续子数组 数组-归并排序-合并有序数组 数组-顺时针打印矩形 数组-24点游戏 链表-链表反转-链表相加 链表-...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值