>  기사  >  웹 프론트엔드  >  Webpack 영구 캐싱 방식에 대한 간략한 토론

Webpack 영구 캐싱 방식에 대한 간략한 토론

亚连
亚连원래의
2018-05-26 16:04:331612검색

이 글에서는 주로 Webpack 영구 캐싱 실습을 소개하고 참고하겠습니다.

머리말

최근에 webpack이 어떻게 지속적 캐싱을 하는지 살펴보았고, 여전히 몇 가지 함정이 있다는 것을 발견했습니다. 이 글을 읽고 나면 대략적으로 이해할 수 있을 것입니다.

  1. 영구 캐싱이란 무엇이며 왜 영구 캐싱을 수행하나요?

  2. webpack은 영구 캐싱을 어떻게 수행하나요?

  3. 웹팩 캐싱에 대한 몇 가지 참고 사항입니다.

영구 캐시

먼저 현재 프런트엔드와 백엔드를 분리하는 애플리케이션이 인기를 끌고 있는 맥락에서 프런트엔드 html, css, js는 정적 리소스 파일인 경우가 많습니다. 양식은 서버에 존재하며 인터페이스를 통해 데이터를 얻어 동적 콘텐츠를 표시합니다. 여기에는 회사가 프런트 엔드 코드를 배포하는 방법에 대한 문제가 포함되므로 업데이트 배포 문제가 포함됩니다. 페이지를 먼저 배포해야 할까요, 아니면 리소스를 먼저 배포해야 할까요?

페이지를 먼저 배포한 다음 리소스 배포: 두 배포 사이의 시간 간격 동안 사용자가 페이지에 액세스하면 이전 리소스가 새 페이지 구조에 로드되고 이전 버전의 리소스가 캐시됩니다. 결과적으로 사용자는 수동으로 새로 고치지 않으면 리소스 캐시가 만료될 때까지 페이지가 무질서한 상태로 유지됩니다.

먼저 리소스 배포 후 페이지 배포: 배포 시간 간격 동안 이전 버전의 리소스에 대한 로컬 캐시가 있는 사용자가 웹 사이트를 방문합니다. 요청한 페이지는 이전 버전이고 리소스 참조는 변경되지 않았으므로 브라우저가 직접 로컬 캐시를 사용하므로 이는 정상적인 상황이지만 로컬 캐시가 없거나 캐시가 만료된 사용자가 웹 사이트를 방문하면 이전 버전 페이지에서 새 버전 리소스를 로드하여 페이지 실행 오류가 발생합니다.

따라서 온라인 코드를 업데이트할 때 온라인 사용자가 원활하게 전환하고 웹 사이트를 올바르게 열 수 있도록 보장하는 배포 전략이 필요합니다.

대기업에서 프런트 엔드 코드를 개발하고 배포하는 방법은 무엇입니까? 이 답변을 먼저 읽어 보는 것이 좋습니다.

위 답변을 읽고 나면 파일이 수정될 때마다 생성되는 해시 값이 다르기 때문에 이제 더 성숙한 영구 캐싱 솔루션은 정적 리소스 이름 뒤에 해시 값을 추가하는 것이라는 것을 대략 이해하게 될 것이며, 이것의 이점은 이전 파일을 덮어쓰고 온라인 사용자 액세스가 실패하는 것을 방지하기 위해 파일을 점진적으로 게시하는 것입니다.

매번 게시되는 정적 리소스(css, js, img)의 이름이 고유하기 때문에 다음을 수행할 수 있습니다.

  1. html 파일의 경우: 캐싱을 활성화하지 않고 내 서버에 html을 넣습니다. 서버의 캐시를 끄세요. 서버는 html 파일과 데이터 인터페이스만 제공합니다

  2. 정적 js, css, 그림 및 기타 파일의 경우: cdn 및 캐시를 활성화하고, cdn 서비스 제공업체에 정적 리소스를 업로드합니다. 장기 캐싱을 켤 수 있습니다. 각 리소스의 경로는 고유하므로 리소스를 덮어쓰지 않아 온라인 사용자 액세스의 안정성이 보장됩니다.

  3. 업데이트가 출시될 때마다 정적 리소스(js, css, img)를 먼저 cdn 서비스로 전송한 후 html 파일을 업로드합니다. 이는 기존 사용자가 정상적으로 액세스할 수 있도록 보장할 뿐만 아니라, 또한 새로운 사용자가 새 페이지에 액세스할 수 있습니다.

위에서는 주류 프런트엔드 영구 캐싱 솔루션을 간략하게 소개했는데 왜 영구 캐싱을 해야 할까요?

사용자가 브라우저를 사용하여 처음으로 사이트를 방문하면 페이지에 다양한 정적 리소스가 표시됩니다. 지속적인 캐싱을 달성할 수 있으면 http 응답 헤더에 Cache-control 또는 Expires 필드를 추가할 수 있습니다. 캐시를 사용하면 브라우저는 이러한 리소스를 로컬에서 하나씩 캐시할 수 있습니다.

사용자가 후속 방문 시 동일한 정적 리소스를 다시 요청해야 하고 정적 리소스가 만료되지 않은 경우 브라우저는 네트워크를 통해 리소스를 요청하는 대신 로컬 캐시를 직접 사용할 수 있습니다.

웹팩이 영구 캐싱을 하는 방법

영구 캐싱을 간략하게 소개한 후 핵심은 다음과 같으니 웹팩에서 영구 캐싱을 어떻게 해야 할까요?

  1. 고유성을 보장해야 할까요? 즉, 패키지된 각 리소스에 대해 고유한 해시 값을 생성합니다. 패키지된 콘텐츠가 일치하지 않는 한 해시 값도 일치하지 않습니다.

  2. 해시 값의 안정성을 보장하려면 모듈이 수정될 때 영향을 받는 패키지 파일의 해시 값만 변경되고 모듈과 관련 없는 패키지 파일의 해시 값은 변경되지 않도록 해야 합니다.

hash 파일 이름은 영구 캐싱을 구현하는 첫 번째 단계입니다. 현재 webpack에는 해시를 계산하는 두 가지 방법([hash] 및 [chunkhash])이 있습니다.

  1. hash는 webpack이 컴파일 중에 매번 해시를 계산한다는 의미입니다. process 프로젝트의 파일이 변경된 후 다시 생성되는 고유한 해시 값을 생성한 다음 webpack이 새 해시 값을 계산합니다.

  2. chunkhash는 모듈을 기준으로 계산된 해시 값이므로 특정 파일에 대한 변경 사항은 자체 해시 값에만 영향을 미치고 다른 파일에는 영향을 미치지 않습니다.

따라서 모든 콘텐츠를 동일한 파일로 압축하면 해시가 만족스러울 수 있습니다. 프로젝트에 압축 풀기, 모듈 로드 등이 포함된 경우 각 업데이트 후에 관련 내용만 확인하기 위해 청크해시를 사용해야 합니다. 파일 해시 값이 변경됩니다.

영구 캐시가 포함된 웹팩 구성은 다음과 같아야 합니다.

module.exports = {
 entry: __dirname + '/src/index.js',
 output: {
 path: __dirname + '/dist',
 filename: '[name].[chunkhash:8].js',
 }
}

위 코드의 의미는 index.js를 진입점으로 사용하고 모든 코드를 index.xxxx라는 파일에 패키징하고 넣는 것입니다. 이제 프로젝트를 업데이트할 때마다 새로운 이름의 파일을 생성할 수 있습니다.

간단한 시나리오를 다루는 경우 이것으로 충분하지만 대규모 다중 페이지 애플리케이션에서는 페이지 성능을 최적화해야 하는 경우가 많습니다.

  1. 별도의 비즈니스 코드와 타사 코드: 비즈니스를 하는 이유 코드 분리 비즈니스 코드는 자주 업데이트되고, 타사 코드는 업데이트 및 반복이 느리기 때문에 타사 코드와 분리되므로 타사 코드(라이브러리, 프레임워크)를 최대한 활용할 수 있도록 분리합니다. 타사 라이브러리를 로드하기 위한 브라우저 캐시입니다.

  2. 요청 시 로드: 예를 들어 React-Router를 사용할 때 사용자가 특정 경로에 액세스해야 하면 해당 구성 요소가 로드됩니다. 그러면 사용자는 처음에 모든 라우팅 구성 요소를 다운로드할 필요가 없습니다. 현지의.

  3. 다중 페이지 애플리케이션에서는 머리글, 바닥글 등과 같은 공통 모듈을 추출할 수 있으므로 페이지가 이동할 때 이러한 공통 모듈은 캐시에 존재하는 대신 직접 로드될 수 있습니다. 더 이상 네트워크 요청을 하는 중입니다.

모듈을 풀고 모듈에 로드하려면 webpack에 내장된 플러그인인 CommonsChunkPlugin이 필요합니다. 아래에서는 예제를 사용하여 webpack을 구성하는 방법을 설명하겠습니다.

이 기사의 코드는 내 Github에 있습니다. 관심이 있으시면 다운로드하여 살펴볼 수 있습니다.

git clone https://github.com/happylindz/blog.git
cd blog/code/multiple-page-webpack-demo
npm install

다음 내용을 읽기 전에 이전 기사를 읽어 보시기 바랍니다: 심층적인 이해 웹팩 파일 패키징 메커니즘 및 웹팩 파일에 대한 이해 패키징 메커니즘은 영구 캐싱을 더 잘 구현하는 데 도움이 됩니다.

예제는 대략 다음과 같이 설명됩니다. 페이지A와 페이지B의 두 페이지로 구성됩니다.

// src/pageA.js
import componentA from './common/componentA';
// 使用到 jquery 第三方库,需要抽离,避免业务打包文件过大
import $ from 'jquery';
// 加载 css 文件,一部分为公共样式,一部分为独有样式,需要抽离
import './css/common.css'
import './css/pageA.css';
console.log(componentA);
console.log($.trim(' do something '));

// src/pageB.js
// 页面 A 和 B 都用到了公共模块 componentA,需要抽离,避免重复加载
import componentA from './common/componentA';
import componentB from './common/componentB';
import './css/common.css'
import './css/pageB.css';
console.log(componentA);
console.log(componentB);
// 用到异步加载模块 asyncComponent,需要抽离,加载首屏速度
document.getElementById('xxxxx').addEventListener('click', () => {
 import( /* webpackChunkName: "async" */
 './common/asyncComponent.js').then((async) => {
  async();
 })
})
// 公共模块基本长这样
export default "component X";

위의 페이지 콘텐츠에는 기본적으로 공용 라이브러리 분할, 요청 시 로드 및 공용 모듈 분할이라는 세 가지 모듈 분할 모드가 포함됩니다. 그런 다음 다음 단계는 웹팩을 구성하는 것입니다.

const path = require('path');

const webpack = require('webpack');

const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {

 entry: {

 pageA: [path.resolve(__dirname, './src/pageA.js')],

 pageB: path.resolve(__dirname, './src/pageB.js'),

 },

 output: {

 path: path.resolve(__dirname, './dist'),

 filename: 'js/[name].[chunkhash:8].js',

 chunkFilename: 'js/[name].[chunkhash:8].js'

 },

 module: {

 rules: [

  {

  // 用正则去匹配要用该 loader 转换的 CSS 文件

  test: /.css$/,

  use: ExtractTextPlugin.extract({

   fallback: "style-loader",

   use: ["css-loader"]

  }) 

  }

 ]

 },

 plugins: [

 new webpack.optimize.CommonsChunkPlugin({

  name: 'common',

  minChunks: 2,

 }),

 new webpack.optimize.CommonsChunkPlugin({

  name: 'vendor',

  minChunks: ({ resource }) => (

  resource && resource.indexOf('node_modules') >= 0 && resource.match(/.js$/)

  )
 }),
 new ExtractTextPlugin({
  filename: `css/[name].[chunkhash:8].css`,
 }),
 ]
}

첫 번째 CommonsChunkPlugin은 공용 모듈을 추출하는 데 사용됩니다. 이는 웹팩 상사에게 모듈이 두 번 이상 로드되는 것을 본다면 해당 모듈을 다음으로 옮기는 데 도움을 주세요. 공통 청크, minChunks는 2이고, 세분성은 실제 상황에 따라 모듈을 분리하는 데 사용해야 하는 횟수를 선택할 수 있습니다.

두 번째 CommonsChunkPlugin은 타사 코드를 추출하고 리소스가 node_modules에서 왔는지 확인하는 데 사용됩니다. 그렇다면 해당 모듈이 타사 모듈임을 의미합니다. 이는 node_modules 디렉터리에서 나오는 일부 모듈을 보고 해당 이름이 .js로 끝나는 경우 이를 공급업체 청크로 이동하라고 말하는 것과 같습니다. 공급업체 청크가 존재하지 않으면 새 모듈을 만드세요.

이 구성의 이점은 무엇입니까? 비즈니스가 성장함에 따라 타사 코드를 저장하기 위한 입구를 특별히 구성하면 webpack.config.js가 더 많이 사용됩니다.

// 不利于拓展
module.exports = {
 entry: {
 app: './src/main.js',
 vendor: [
  'vue',
  'axio',
  'vue-router',
  'vuex',
  // more
 ],
 },
}

세 번째 ExtractTextPlugin 플러그인은 패키지된 js 파일에서 CSS를 추출하고 독립적인 CSS 파일을 생성하는 데 사용됩니다. 스타일만 수정하면 페이지를 수정하는 기능이 없다고 상상해 보세요. 당신은 확실히 js 파일의 해시 값이 변경되는 것을 원하지 않습니다. CSS와 js가 서로 분리되어 서로 영향을 미치지 않기를 바랍니다.

webpack을 실행한 후 패키징 효과를 볼 수 있습니다.

├── css

│ ├── common.2beb7387.css

│ ├── pageA.d178426d.css

│ └── pageB.33931188.css

└── js

 ├── async.03f28faf.js

 ├── common.2beb7387.js

 ├── pageA.d178426d.js

 ├── pageB.33931188.js

 └── vendor.22a1d956.js

Css와 js가 분리된 것을 볼 수 있으며 모듈 청크의 고유성을 보장하기 위해 모듈을 분할했습니다. 코드를 업데이트할 때마다 다른 해시 값.

고유성을 통해 해시 값의 안정성을 보장해야 합니다. 이 시나리오를 상상해 보세요. 코드의 특정 부분(모듈, CSS)을 수정하여 파일의 해시 값이 변경되는 것을 원하지 않을 것입니다. . 그래서 당연히 현명하지 못한 일인데, 해시 값의 변화를 최소화하려면 어떻게 해야 할까요?

즉, 웹팩 컴파일에서 캐시 실패를 일으키는 요인을 찾아내고, 이를 해결하거나 최적화할 수 있는 방법을 찾아야 한다는 것인가요?

Chunkhash 값의 변화는 주로 다음 네 가지 부분에 의해 발생합니다.

  1. 모듈이 포함된 소스 코드

  2. webpack 실행을 시작하는 데 사용되는 런타임 코드

  3. webpack에서 생성된 모듈 moduleid( 모듈 ID 및 참조된 종속 모듈 ID 포함)

  4. chunkID

이 네 부분 중 일부가 변경되는 한 생성된 청크 파일은 달라지며 캐시는 유효하지 않게 됩니다. 4개 부분. 1. 소개:

1. 소스 코드 변경:

显然不用多说,缓存必须要刷新,不然就有问题了

二、webpack 启动运行的 runtime 代码:

看过我之前的文章:深入理解 webpack 文件打包机制 就会知道,在 webpack 启动的时候需要执行一些启动代码。

(function(modules) {

 window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {

 // ...

 };

 function __webpack_require__(moduleId) {

 // ...

 }

 __webpack_require__.e = function requireEnsure(chunkId, callback) {

 // ...

 script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"pageA","1":"pageB","3":"vendor"}[chunkId]||chunkId) + "." + {"0":"e72ce7d4","1":"69f6bbe3","2":"9adbbaa0","3":"53fa02a7"}[chunkId] + ".js";

 };

})([]);

大致内容像上面这样,它们是 webpack 的一些启动代码,它们是一些函数,告诉浏览器如何加载 webpack 定义的模块。

其中有一行代码每次更新都会改变的,因为启动代码需要清楚地知道 chunkid 和 chunkhash 值得对应关系,这样在异步加载的时候才能正确地拼接出异步 js 文件的路径。

那么这部分代码最终放在哪个文件呢?因为我们刚才配置的时候最后生成的 common chunk 模块,那么这部分运行时代码会被直接内置在里面,这就导致了,我们每次更新我们业务代码(pageA, pageB, 模块)的时候, common chunkhash 会一直变化,但是这显然不符合我们的设想,因为我们只是要用 common chunk 用来存放公共模块(这里指的是 componentA),那么我 componentA 都没去修改,凭啥 chunkhash 需要变了。

所以我们需要将这部分 runtime 代码抽离成单独文件。

module.exports = {

 // ...

 plugins: [

 // ...

 // 放到其他的 CommonsChunkPlugin 后面

 new webpack.optimize.CommonsChunkPlugin({

  name: 'runtime',

  minChunks: Infinity,
 }),
 ]
}

这相当于是告诉 webpack 帮我把运行时代码抽离,放到单独的文件中。

├── css

│ ├── common.4cc08e4d.css

│ ├── pageA.d178426d.css

│ └── pageB.33931188.css

└── js

 ├── async.03f28faf.js

 ├── common.4cc08e4d.js

 ├── pageA.d178426d.js

 ├── pageB.33931188.js

 ├── runtime.8c79fdcd.js

 └── vendor.cef44292.js

多生成了一个 runtime.xxxx.js,以后你在改动业务代码的时候,common chunk 的 hash 值就不会变了,取而代之的是 runtime chunk hash 值会变,既然这部分代码是动态的,可以通过 chunk-manifest-webpack-plugin 将他们 inline 到 html 中,减少一次网络请求。

三、webpack 生成的模块 moduleid

在 webpack2 中默认加载 OccurrenceOrderPlugin 这个插件,OccurrenceOrderPlugin 插件会按引入次数最多的模块进行排序,引入次数的模块的 moduleId 越小,但是这仍然是不稳定的,随着你代码量的增加,虽然代码引用次数的模块 moduleId 越小,越不容易变化,但是难免还是不确定的。

默认情况下,模块的 id 是这个模块在模块数组中的索引。OccurenceOrderPlugin 会将引用次数多的模块放在前面,在每次编译时模块的顺序都是一致的,如果你修改代码时新增或删除了一些模块,这将可能会影响到所有模块的 id。

最佳实践方案是通过 HashedModuleIdsPlugin 这个插件,这个插件会根据模块的相对路径生成一个长度只有四位的字符串作为模块的 id,既隐藏了模块的路径信息,又减少了模块 id 的长度。

这样一来,改变 moduleId 的方式就只有文件路径的改变了,只要你的文件路径值不变,生成四位的字符串就不变,hash 值也不变。增加或删除业务代码模块不会对 moduleid 产生任何影响。

module.exports = {

 plugins: [

 new webpack.HashedModuleIdsPlugin(),

 // 放在最前面

 // ...

 ]

}

四、chunkID

实际情况中分块的个数的顺序在多次编译之间大多都是固定的, 不太容易发生变化。

这里涉及的只是比较基础的模块拆分,还有一些其它情况没有考虑到,比如异步加载组件中包含公共模块,可以再次将公共模块进行抽离。形成异步公共 chunk 模块。有想深入学习的可以看这篇文章:Webpack 大法之 Code Splitting

webpack 做缓存的一些注意点

  1. CSS 文件 hash 值失效的问题

  2. 不建议线上发布使用 DllPlugin 插件

CSS 文件 hash 值失效的问题:

ExtractTextPlugin 有个比较严重的问题,那就是它生成文件名所用的[chunkhash]是直接取自于引用该 css 代码段的 js chunk ;换句话说,如果我只是修改 css 代码段,而不动 js 代码,那么最后生成出来的 css 文件名依然没有变化。

所以我们需要将 ExtractTextPlugin 中的 chunkhash 改为 contenthash,顾名思义,contenthash 代表的是文本文件内容的 hash 值,也就是只有 style 文件的 hash 值。这样编译出来的 js 和 css 文件就有独立的 hash 值了。

module.exports = {

 plugins: [

 // ...

 new ExtractTextPlugin({

  filename: `css/[name].[contenthash:8].css`,

 }),

 ]

}

如果你使用的是 webpack2,webpack3,那么恭喜你,这样就足够了,js 文件和 css 文件修改都不会影响到相互的 hash 值。那如果你使用的是 webpack1,那么就会出现问题。

具体来讲就是 webpack1 和 webpack 在计算 chunkhash 值得不同:

webpack1 在涉及的时候并没有考虑像 ExtractTextPlugin 会将模块内容抽离的问题,所以它在计算 chunkhash 的时候是通过打包之前模块内容去计算的,也就是说在计算的时候 css 内容也包含在内,之后才将 css 内容抽离成单独的文件,

那么就会出现:如果只修改了 css 文件,未修改引用的 js 文件,那么编译输出的 js 文件的 hash 值也会改变。

이와 관련하여 webpack2에서는 패키지된 파일 내용을 기준으로 해시 값을 계산하므로 ExtractTextPlugin이 CSS 코드를 추출한 후이므로 위와 같은 문제는 없습니다. 안타깝게도 아직 webpack1을 사용하고 있다면 md5-hash-webpack-plugin 플러그인을 사용하여 webpack의 해시 계산 전략을 변경하는 것이 좋습니다.

온라인 게시에는 DllPlugin 플러그인을 사용하지 않는 것이 좋습니다

왜 그렇게 말씀하시나요? 최근에 한 친구가 저에게 와서 왜 그들의 리더가 DllPlugin 플러그인을 온라인에서 사용하는 것을 허용하지 않는지 물었습니다.

DllPlugin 자체에는 몇 가지 단점이 있습니다.

  1. 우선 추가 웹팩 구성을 구성해야 하므로 작업량이 늘어납니다.

  2. 페이지 중 하나는 매우 큰 타사 종속성 라이브러리를 사용하고 다른 페이지는 이를 전혀 사용할 필요가 없습니다. 하지만 dll.js에 직접 패키징할 가치는 없습니다. 이 쓸모없는 코드는 webpack2의 코드 분할 기능을 사용할 수 없습니다.

  3. 처음 열 때 dll 파일을 다운로드해야 합니다. 많은 라이브러리를 함께 묶었기 때문에 dll 파일이 매우 커지고 첫 페이지의 로딩 속도가 매우 느려집니다.

다음에 캐시를 요청할 필요가 없도록 dll 파일로 패키징한 다음 브라우저에서 캐시를 읽도록 할 수 있지만 예를 들어 lodash 기능 중 하나를 사용하는 경우 dll을 사용하면 전체 lodash 파일이 입력됩니다. 이로 인해 불필요한 코드가 너무 많이 로드되어 첫 번째 화면 렌더링 시간에 도움이 되지 않습니다.

올바른 접근 방식은 다음과 같습니다.

  1. React 및 Vue와 같은 강력한 무결성을 갖춘 라이브러리는 캐싱을 위한 공급업체 타사 라이브러리를 생성할 수 있습니다. 일반 기술 시스템이 하나의 사이트로 고정되어 있기 때문입니다. 기본적으로 통합 기술 시스템은 사용되므로 캐싱을 위해 공급업체 라이브러리가 생성됩니다.

  2. antd 및 lodash와 같은 기능적 구성 요소 라이브러리는 트리 쉐이킹을 통해 제거될 수 있으므로 유용한 코드만 남게 됩니다. 공급업체의 타사 라이브러리에 직접 연결하지 마세요. 그렇지 않으면 쓸모없는 코드가 많이 실행됩니다.

결론

그래요, 최근에 웹팩을 읽으면서 정말 많은 것을 얻었던 것 같습니다. 모두가 이 글을 통해 뭔가를 얻었으면 좋겠습니다. 또한, 파일 캐싱 메커니즘을 이해하는 데 더 도움이 될 수 있는 이전에 쓴 글을 다시 추천합니다. 웹팩 파일 패키징 메커니즘에 대한 심층적인 이해

위 내용은 모두에게 도움이 되기를 바랍니다. 앞으로도 모든 사람에게.

관련 기사:

아주 실용적인 ajax 사용자 등록 모듈

Ajax를 클릭하면 데이터 목록을 지속적으로 로드할 수 있습니다(그래픽 튜토리얼).

Ajax+Struts2를 사용하여 인증 코드 확인 기능을 구현합니다(그래픽 튜토리얼)

위 내용은 Webpack 영구 캐싱 방식에 대한 간략한 토론의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.