> 웹 프론트엔드 > JS 튜토리얼 > 웹팩을 사용하여 리소스 방법 및 기술을 최적화하는 방법

웹팩을 사용하여 리소스 방법 및 기술을 최적화하는 방법

小云云
풀어 주다: 2018-01-04 14:16:37
원래의
1662명이 탐색했습니다.

프런트 엔드 애플리케이션 최적화에서는 로드된 리소스의 크기를 제어하는 ​​것이 매우 중요합니다. 대부분의 경우 우리가 할 수 있는 일은 패키징 및 컴파일 프로세스 중에 크기를 제어하고 리소스를 분할하고 재사용하는 것입니다. 이번 글에서는 webpack을 활용하여 리소스를 최적화하는 방법을 주로 소개하고 있는데, 에디터가 꽤 괜찮다고 생각해서 지금부터 공유하고 참고용으로 올려보겠습니다. 편집자를 따라 살펴보겠습니다. 모두에게 도움이 되기를 바랍니다.

머리말

이 글은 주로 웹팩 패키징을 기반으로 하며, 웹팩 패키징 수준에서 리소스와 캐시를 처리하는 방법을 설명하기 위해 React, vue 및 기타 생태학적 환경에서 개발된 단일 페이지 애플리케이션을 사용합니다. 해야 할 일은 소량의 비즈니스 코드 변경을 포함하면서 웹팩 구성 최적화를 수행하는 것입니다.

동시에 (webpack-contrib/webpack-bundle-analyzer) 플러그인을 사용하여 패키지된 리소스를 분석할 수 있습니다. 물론 이 기사에서는 이 플러그인을 선택적으로 사용할 수 있습니다. 주로 예시로 사용됩니다.

팁: 웹팩 버전 @3.6.0

1. 패키징 환경 및 코드 압축

먼저 기본 웹팩 구성이 있습니다:

// webpack.config.js
const path = require('path');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const PROJECT_ROOT = path.resolve(__dirname, './');

module.exports = {
 entry: {
  index: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[chunkhash:4].js'
 },
 module: {
  rules: [
   {
    test: /\.js[x]?$/,
    use: 'babel-loader',
    include: PROJECT_ROOT,
    exclude: /node_modules/
   }
  ]
 },
 plugins: [
  new BundleAnalyzerPlugin()
 ],
 resolve: {
  extensions: ['.js', '.jsx']
 },
};
로그인 후 복사

패키징을 실행하면 프로젝트의 js가 100만 개가 넘는 것을 확인할 수 있습니다.

Hash: e51afc2635f08322670b
Version: webpack 3.6.0
Time: 2769ms
    Asset  Size Chunks          Chunk Names
index.caa7.js 1.3 MB    0 [emitted] [big] index
로그인 후 복사

이때 플러그인 `DefinePlugin` 및 `UglifyJSPlugin`만 추가하면 볼륨을 많이 줄일 수 있습니다. 플러그인에

// webpack.config.js
...
{
 ...
 plugins: [
  new BundleAnalyzerPlugin(),
  new webpack.DefinePlugin({
   'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  new UglifyJSPlugin({
   uglifyOptions: {
    ie8: false,
    output: {
     comments: false,
     beautify: false,
    },
    mangle: {
     keep_fnames: true
    },
    compress: {
     warnings: false,
     drop_console: true
    },
   }
  })
 ]
 ...
}
로그인 후 복사

를 추가하면 이때 패키징 출력을 볼 수 있습니다.

Hash: 84338998472a6d3c5c25
Version: webpack 3.6.0
Time: 9940ms
    Asset  Size Chunks          Chunk Names
index.89c2.js 346 kB    0 [emitted] [big] index
로그인 후 복사

코드 크기가 1.3M에서 346K로 줄었습니다.

(1)DefinePlugin

DefinePlugin을 사용하면 컴파일 타임에 구성할 수 있는 전역 상수를 생성할 수 있습니다. 이는 개발 모드와 릴리스 모드 빌드에 대해 서로 다른 동작을 허용하는 데 유용할 수 있습니다. 개발 빌드에서는 로깅을 수행하지만 릴리스 빌드에서는 수행하지 않는 경우 전역 상수를 사용하여 로그 여부를 결정할 수 있습니다. 이것이 DefinePlugin이 들어오는 곳이며, 이를 설정하고 개발 및 릴리스 빌드에 대한 규칙은 잊어버리세요.

비즈니스 코드 및 타사 패키지 코드에서는 다른 처리를 수행하기 위해 `process.env.NODE_ENV`를 판단해야 하는 경우가 많지만 프로덕션 환경에서는 분명히 `프로덕션`이 아닌 처리 부분이 필요하지 않습니다.

여기에서는 `process.env.NODE_ENV`를 `JSON.stringify('production')`으로 설정했는데, 이는 패키징 환경이 프로덕션 환경으로 설정되었음을 의미합니다. 그런 다음 'UglifyJSPlugin' 플러그인을 사용하면 프로덕션 환경용으로 패키징할 때 일부 중복 코드를 제거할 수 있습니다.

(2) UglifyJSPlugin

UglifyJSPlugin은 js 코드를 구문 분석하고 압축하는 데 주로 사용됩니다. js 코드를 처리하기 위해 `uglify-es`를 기반으로 합니다. https://github.com/webpack. - contrib/uglifyjs-webpack-plugin.

코드를 압축하고 중복성을 제거함으로써 패키지된 리소스의 크기가 크게 줄어듭니다.

2. 코드 분할/주문형 로딩

React 또는 Vue로 구축된 단일 페이지 애플리케이션에서 페이지 라우팅 및 뷰 제어는 프런트 엔드에서 구현되며 해당 비즈니스 로직은 js 코드에 있습니다.

애플리케이션 디자인에 페이지와 로직이 많으면 최종 생성되는 js 파일 리소스도 상당히 커집니다.

그러나 URL에 해당하는 페이지를 열 때 실제로는 모든 js 코드가 필요하지 않습니다. 우리에게 필요한 것은 다음 뷰를 로드할 때 메인 런타임 코드와 해당 비즈니스 로직 코드뿐입니다. 해당 코드 부분을 로드합니다.

따라서 이와 관련하여 수행할 수 있는 최적화는 요청 시 js 코드를 로드하는 것입니다.

지연 로딩 또는 주문형 로딩은 웹 페이지나 애플리케이션을 최적화하는 좋은 방법입니다. 이 방법은 실제로 일부 논리적 중단점에서 코드를 분리한 다음 일부 코드 블록에서 특정 작업을 완료한 후 즉시 다른 새 코드 블록을 참조하거나 참조하려고 합니다. 이렇게 하면 특정 코드 블록이 로드되지 않을 수 있으므로 앱의 초기 로드 속도가 빨라지고 전체 크기가 줄어듭니다.

Webpack은 코드 분할을 달성하기 위해 동적 가져오기 기술을 제공합니다. 먼저 webpack 구성에서 각 분할 하위 모듈의 구성을 구성해야 합니다.

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const PROJECT_ROOT = path.resolve(__dirname, './');

module.exports = {
 entry: {
  index: './src/index.js'
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[chunkhash:4].js',
  chunkFilename: '[name].[chunkhash:4].child.js',
 },
 module: {
  rules: [
   {
    test: /\.js[x]?$/,
    use: 'babel-loader',
    include: PROJECT_ROOT,
    exclude: /node_modules/
   }
  ]
 },
 plugins: [
  new BundleAnalyzerPlugin(),
  new webpack.DefinePlugin({
   'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  new UglifyJSPlugin({
   uglifyOptions: {
    ie8: false,
    output: {
     comments: false,
     beautify: false,
    },
    mangle: {
     keep_fnames: true
    },
    compress: {
     warnings: false,
     drop_console: true
    },
   }
  }),
 ],
 resolve: {
  extensions: ['.js', '.jsx']
 },
};
로그인 후 복사

정의해야 할 주요 사항은 '출력'입니다. ChunkFilename`은 내보낸 분할 코드의 파일 이름입니다. 여기서는 `[name].[chunkhash:4].child.js`로 설정되어 있습니다. 여기서 `name`은 모듈 이름 또는 ID, `chunkhash에 해당합니다. `는 모듈 콘텐츠의 해시입니다.

나중에 webpack은 비즈니스 코드에서 동적으로 가져오는 두 가지 방법을 제공합니다:

  • `import('path/to/module') -> Promise`,

  • `require.ensure(종속성: 문자열 [] , callback: function(require), errorCallback: function(error), ChunkName: String)`

최신 버전의 webpack에서는 주로 `import()` 사용을 권장합니다(참고: import는 Promise를 사용하므로 코드에서 Promise 폴리필이 지원되는지 확인해야 합니다.

// src/index.js
function getComponent() {
 return import(
  /* webpackChunkName: "lodash" */
  'lodash'
 ).then(_ => {
  var element = document.createElement('p');

  element.innerHTML = _.join(['Hello', 'webpack'], ' ');

  return element;

 }).catch(error => 'An error occurred while loading the component');
}

getComponent().then(component => {
 document.body.appendChild(component);
})
로그인 후 복사

패키징 정보를 볼 수 있습니다:

Hash: d6ba79fe5995bcf9fa4d
Version: webpack 3.6.0
Time: 7022ms
        Asset   Size Chunks       Chunk Names
lodash.89f0.child.js 85.4 kB    0 [emitted] lodash
    index.316e.js 1.96 kB    1 [emitted] index
  [0] ./src/index.js 441 bytes {1} [built]
  [2] (webpack)/buildin/global.js 509 bytes {0} [built]
  [3] (webpack)/buildin/module.js 517 bytes {0} [built]
  + 1 hidden module
로그인 후 복사

패키징된 코드가 `index.316e.js`와 `lodash.89f0.child.js`라는 두 개의 파일을 생성하는 것을 볼 수 있으며, 후자는 `import`를 통해 전달됩니다. 분할을 달성합니다.

`import` 它接收一个 `path` 参数,指的是该子模块对于的路径,同时还注意到其中可以添加一行注释 `/* webpackChunkName: "lodash" */`,该注释并非是无用的,它定义了该子模块的 name,其对应与 `output.chunkFilename` 中的 `[name]`。

`import` 函数返回一个 Promise,当异步加载到子模块代码是会执行后续操作,比如更新视图等。

(1)React 中的按需加载

在 React 配合 React-Router 开发中,往往就需要代码根据路由按需加载的能力,下面是一个基于 webpack 代码动态导入技术实现的 React 动态载入的组件:

import React, { Component } from 'react';

export default function lazyLoader (importComponent) {
 class AsyncComponent extends Component {
  state = { Component: null }

  async componentDidMount () {
   const { default: Component } = await importComponent();

   this.setState({
    Component: Component
   });
  }

  render () {
   const Component = this.state.Component;

   return Component
    ? <Component {...this.props} />
    : null;
  }
 }

 return AsyncComponent;
};
로그인 후 복사

在 `Route` 中:

<Switch>
 <Route exact path="/"
  component={lazyLoader(() => import('./Home'))}
 />
 <Route path="/about"
  component={lazyLoader(() => import('./About'))}
 />
 <Route
  component={lazyLoader(() => import('./NotFound'))}
 />
</Switch>
로그인 후 복사

在 `Route` 中渲染的是 `lazyLoader` 函数返回的组件,该组件在 mount 之后会去执行 `importComponent` 函数(既:`() => import('./About')`)动态加载其对于的组件模块(被拆分出来的代码),待加载成功之后渲染该组件。

使用该方式打包出来的代码:

Hash: 02a053d135a5653de985
Version: webpack 3.6.0
Time: 9399ms
     Asset   Size Chunks          Chunk Names
0.db22.child.js 5.82 kB    0 [emitted]
1.fcf5.child.js  4.4 kB    1 [emitted]
2.442d.child.js   3 kB    2 [emitted]
 index.1bbc.js  339 kB    3 [emitted] [big] index
로그인 후 복사

三、抽离 Common 资源

(1)第三方库的长缓存

首先对于一些比较大的第三方库,比如在 React 中用到的 react、react-dom、react-router 等等,我们不希望它们被重复打包,并且在每次版本更新的时候也不希望去改变这部分的资源导致在用户端重新加载。

在这里可以使用 webpack 的 CommonsChunkPlugin 来抽离这些公共资源;

CommonsChunkPlugin 插件,是一个可选的用于建立一个独立文件(又称作 chunk)的功能,这个文件包括多个入口 chunk 的公共模块。通过将公共模块拆出来,最终合成的文件能够在最开始的时候加载一次,便存起来到缓存中供后续使用。这个带来速度上的提升,因为浏览器会迅速将公共的代码从缓存中取出来,而不是每次访问一个新页面时,再去加载一个更大的文件。
首先需要在 entry 中新增一个入口用来打包需要抽离出来的库,这里将 `'react', 'react-dom', 'react-router-dom', 'immutable'` 都给单独打包进 `vendor` 中;

之后在 plugins 中定义一个 `CommonsChunkPlugin` 插件,同时将其 `name` 设置为 `vendor` 是它们相关联,再将 `minChunks` 设置为 `Infinity` 防止其他代码被打包进来。

// webpack.config.js
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const PROJECT_ROOT = path.resolve(__dirname, './');

module.exports = {
 entry: {
  index: './src0/index.js',
  vendor: ['react', 'react-dom', 'react-router-dom', 'immutable']
 },
 output: {
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].[chunkhash:4].js',
  chunkFilename: '[name].[chunkhash:4].child.js',
 },
 module: {
  rules: [
   {
    test: /\.js[x]?$/,
    use: 'babel-loader',
    include: PROJECT_ROOT,
    exclude: /node_modules/
   }
  ]
 },
 plugins: [
  new BundleAnalyzerPlugin(),
  new webpack.DefinePlugin({
   'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
  }),
  new UglifyJSPlugin({
   uglifyOptions: {
    ie8: false,
    output: {
     comments: false,
     beautify: false,
    },
    mangle: {
     keep_fnames: true
    },
    compress: {
     warnings: false,
     drop_console: true
    },
   }
  }),
  new webpack.optimize.CommonsChunkPlugin({
   name: 'vendor',
   minChunks: Infinity,
  }),
 ],
 resolve: {
  extensions: ['.js', '.jsx']
 },
};
로그인 후 복사

运行打包可以看到:

Hash: 34a71fcfd9a24e810c21
Version: webpack 3.6.0
Time: 9618ms
     Asset   Size Chunks          Chunk Names
0.2c65.child.js 5.82 kB    0 [emitted]
1.6e26.child.js  4.4 kB    1 [emitted]
2.e4bc.child.js   3 kB    2 [emitted]
 index.4e2f.js 64.2 kB    3 [emitted]     index
 vendor.5fd1.js  276 kB    4 [emitted] [big] vendor
로그인 후 복사

可以看到 `vendor` 被单独打包出来了。

当我们改变业务代码时再次打包:

Hash: cd3f1bc16b28ac97e20a
Version: webpack 3.6.0
Time: 9750ms
     Asset   Size Chunks          Chunk Names
0.2c65.child.js 5.82 kB    0 [emitted]
1.6e26.child.js  4.4 kB    1 [emitted]
2.e4bc.child.js   3 kB    2 [emitted]
 index.4d45.js 64.2 kB    3 [emitted]     index
 vendor.bc85.js  276 kB    4 [emitted] [big] vendor
로그인 후 복사

vendor 包同样被打包出来的,然而它的文件 hash 却发生了变化,这显然不符合我们长缓存的需求。

这是因为 webpack 在使用 CommoChunkPlugin 的时候会生成一段 runtime 代码(它主要用来处理代码模块的映射关系),而哪怕没有改变 vendor 里的代码,这个 runtime 仍然是会跟随着打包变化的并且打入 verdor 中,所以 hash 就会开始变化了。解决方案则是把这部分的 runtime 代码也单独抽离出来,修改之前的 `CommonsChunkPlugin` 为:

// webpack.config.js
...
new webpack.optimize.CommonsChunkPlugin({
 name: ['vendor', 'runtime'],
 minChunks: Infinity,
}),
...
로그인 후 복사

执行打包可以看到生成的代码中多了 `runtime` 文件,同时即使改变业务代码,vendor 的 hash 值也保持不变了。

当然这段 `runtime` 实际上非常短,我们可以直接 inline 在 html 中,如果使用的是 `html-webpack-plugin` 插件处理 html,则可以结合 [`html-webpack-inline-source-plugin`](DustinJackson/html-webpack-inline-source-plugin) 插件自动处理其 inline。

(2)公共资源抽离

在我们打包出来的 js 资源包括不同入口以及子模块的 js 资源包,然而它们之间也会重复载入相同的依赖模块或者代码,因此可以通过 CommonsChunkPlugin 插件将它们共同依赖的一些资源打包成一个公共的 js 资源。

// webpack.config.js
plugins: [
 new BundleAnalyzerPlugin(),
 new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
 }),
 new UglifyJSPlugin({
  uglifyOptions: {
   ie8: false,
   output: {
    comments: false,
    beautify: false,
   },
   mangle: {
    keep_fnames: true
   },
   compress: {
    warnings: false,
    drop_console: true
   },
  }
 }),
 new webpack.optimize.CommonsChunkPlugin({
  name: ['vendor', 'runtime'],
  minChunks: Infinity,
 }),
 new webpack.optimize.CommonsChunkPlugin({
  // ( 公共chunk(commnons chunk) 的名称)
  name: "commons",
  // ( 公共chunk 的文件名)
  filename: "commons.[chunkhash:4].js",
  // (模块必须被 3个 入口chunk 共享)
  minChunks: 3
 })
],
로그인 후 복사

可以看到这里增加了 `commons` 的一个打包,当一个资源被三个以及以上 chunk 依赖时,这些资源会被单独抽离打包到 `commons.[chunkhash:4].js` 文件。

执行打包,看到结果如下:

Hash: 2577e42dc5d8b94114c8
Version: webpack 3.6.0
Time: 24009ms
     Asset   Size Chunks          Chunk Names
0.2eee.child.js 90.8 kB    0 [emitted]
1.cfbc.child.js 89.4 kB    1 [emitted]
2.557a.child.js  88 kB    2 [emitted]
 vendor.66fd.js  275 kB    3 [emitted] [big] vendor
 index.688b.js 64.2 kB    4 [emitted]     index
commons.a61e.js 1.78 kB    5 [emitted]     commons
로그인 후 복사

却发现这里的 `commons.[chunkhash].js` 基本没有实际内容,然而明明在每个子模块中也都依赖了一些相同的依赖。

借助 webpack-bundle-analyzer 来分析一波:

可以看到三个模块都依赖了 `lodash`,然而它并没有被抽离出来。

这是因为 CommonsChunkPlugin 中的 chunk 指的是 entry 中的每个入口,因此对于一个入口拆分出来的子模块(children chunk)是不生效的。

可以通过在 CommonsChunkPlugin 插件中配置 `children` 参数将拆分出来的子模块的公共依赖也打包进 `commons` 中:

// webpack.config.js
plugins: [
 new BundleAnalyzerPlugin(),
 new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
 }),
 new UglifyJSPlugin({
  uglifyOptions: {
   ie8: false,
   output: {
    comments: false,
    beautify: false,
   },
   mangle: {
    keep_fnames: true
   },
   compress: {
    warnings: false,
    drop_console: true
   },
  }
 }),
 new webpack.optimize.CommonsChunkPlugin({
  name: ['vendor', 'runtime'],
  minChunks: Infinity,
 }),
 new webpack.optimize.CommonsChunkPlugin({
  // ( 公共chunk(commnons chunk) 的名称)
  name: "commons",
  // ( 公共chunk 的文件名)
  filename: "commons.[chunkhash:4].js",
  // (模块必须被 3个 入口chunk 共享)
  minChunks: 3
 }),
 new webpack.optimize.CommonsChunkPlugin({
  // (选择所有被选 chunks 的子 chunks)
  children: true,
  // (在提取之前需要至少三个子 chunk 共享这个模块)
  minChunks: 3,
 })
],
로그인 후 복사

查看打包效果:

其子模块的公共资源都被打包到 `index` 之中了,并没有理想地打包进 `commons` 之中,还是因为 `commons` 对于的是 entry 中的入口模块,而这里并未有 3 个 entry 模块共用资源;

在单入口的应用中可以选择去除 `commons`,而在子模块的 `CommonsChunkPlugin` 的配置中配置 `async` 为 `true`:

// webpack.config.js
plugins: [
 new BundleAnalyzerPlugin(),
 new webpack.DefinePlugin({
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production')
 }),
 new UglifyJSPlugin({
  uglifyOptions: {
   ie8: false,
   output: {
    comments: false,
    beautify: false,
   },
   mangle: {
    keep_fnames: true
   },
   compress: {
    warnings: false,
    drop_console: true
   },
  }
 }),
 new webpack.optimize.CommonsChunkPlugin({
  name: ['vendor', 'runtime'],
  minChunks: Infinity,
 }),
 new webpack.optimize.CommonsChunkPlugin({
  // (选择所有被选 chunks 的子 chunks)
  children: true,
  // (异步加载)
  async: true,
  // (在提取之前需要至少三个子 chunk 共享这个模块)
  minChunks: 3,
 })
],
로그인 후 복사

查看效果:

子模块的公共资源都被打包到 `0.9c90.child.js` 中了,该模块则是子模块的 commons。

四、tree shaking

tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。这个术语和概念实际上是兴起于 ES2015 模块打包工具 rollup。
在我们引入一个依赖的某个输出的时候,我们可能需要的仅仅是该依赖的某一部分代码,而另一部分代码则是 `unused` 的,如果能够去除这部分代码,那么最终打包出来的资源体积也是可以有可观的减小。

首先,webpack 中实现 tree shaking 是基于 webpack 内部支持的 es2015 的模块机制,在大部分时候我们使用 babel 来编译 js 代码,而 babel 会通过自己的模块加载机制处理一遍,这导致 webpack 中的 tree shaking 处理将会失效。因此在 babel 的配置中需要关闭对模块加载的处理:

// .babelrc
{
 "presets": [
  [
   "env", {
    "modules": false,
   }
  ],
  "stage-0"
 ],
 ...
}
로그인 후 복사

然后我们来看下 webpack 是如何处理打包的代码,举例有一个入口文件 `index.js` 和一个 `utils.js` 文件:

// utils.js
export function square(x) {
 return x * x;
}

export function cube(x) {
 return x * x * x;
}
"

"js
// index.js
import { cube } from './utils.js';

console.log(cube(10));
"

打包出来的代码:

"
// index.bundle.js
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
/* unused harmony export square */
/* harmony export (immutable) */ __webpack_exports__["a"] = cube;
function square(x) {
 return x * x;
}

function cube(x) {
 return x * x * x;
}
로그인 후 복사

可以看到仅有 `cube` 函数被 `__webpack_exports__` 导出来,而 `square` 函数被标记为 `unused harmony export square`,然而在打包代码中既是 `square` 没有被导出但是它仍然存在与代码中,而如何去除其代码则可以通过添加 `UglifyjsWebpackPlugin` 插件来处理。

相关推荐:

实例详解Webpack优化配置缩小文件搜索范围

webpack性能优化

webpack配置之后端渲染详解

위 내용은 웹팩을 사용하여 리소스 방법 및 기술을 최적화하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿