Webpack 增量打包

发布于 2022-11-01 22:04:10 字数 35344 浏览 132 评论 0

一,Webpack 打包存在的问题

Webpack 的打包顺序:

var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].js"
    }
};
  1. 找到入口文件
  2. 根据入口文件,找出具有依赖关系的文件 js/css
  3. 最后,把 css/js 全部打包成一个js包

好的,打包完成,打包了整个世界,那么问题来了:

产品说:按钮颜色不对,给我改成 #ccc

技术:好的,这就改。

然后就有了如下流程:

  1. 找到了 entry -> js -> componet -> button.less 修改了一个色值
  2. 执行 webpack 打包

这时就暴露了问题:

  1. 明明只是修改了一个色值,却要从入口开始重新打包
  2. 业务代码明明没有变化,却也被牵连了
  3. 最后生成的 js 要全部推到线上,覆盖掉线上原本没问题的业务 js,纯粹是增加风险

二,思考解决方案

首先想到的是,既然只修改一个文件,那能不能重新打包一个文件呢?

1. 单独打包更新的文件?

这种方案,很快就被自我否定了。

因为:

  1. 从入口打包的文件,已经通过依赖关系,把老版本 button.less 打入了最终输出的 js 文件
  2. 单独打包 button.less 输出了一个独立的 button.js,这个文件需要手动引入到 html 中,一旦这类文件制造的多了,根本无法维护

经过反复思考,单独打包每一个文件的想法,不符合 webpack 的设计初衷,从入口打包的流程是不能够产生变化的

在这个问题上卡了真的很久.....很久

2. 能否通过依赖关系,打包某一个入口?

由于我面临的场景是多页应用,所以存在多个入口,那么既然如此,那么能否通过依赖关系,找到需要更新的入口呢?

这种方案,也思索了很久,后来也被否定了。

因为:

  1. webpack 没有适合输出模块依赖关系的插件,遍寻无果啊
  2. 通过 webpack 的 stats 分析指标,能够输出依赖关系,但数据量太大,如果不加过滤,目前项目输出 12W 行 json 信息,还需要花力气处理一遍这个信息才能拿到关系
  3. 如果一个组件被多个入口引用,那么需要找到每一个引用的入口点,再重新打包每个被波及的入口

上面尤其是第三点,完全不符合我们想增量发布的目的,如果改了一个 button 组件,要重新打包二三十个入口,这完全没有增量发布的意义

在这个问题上又纠结了很久......很久

三,合理的解决方案

经过前面两个问题后,我发现思考的方向完全是错误的,总是妄想改变 webpack 的打包方式,简直就是跟它的理念对着干。

既然不能改变 webpack 的打包方式,那么我能否改变 webpack 的输出结果呢?

其实 webpack 关于缓存方面的功能,提供了很多功能强大的插件,例如:

  • CommonsChunkPlugin 可以用来在打包的时候提取公共 js 代码
  • ExtractTextPlugin 可以用来从 js 中提出 css,将其输出到一个独立的文件

利用这两个插件,我们能够将我们打包的精度加以划分,将公共引用的部分打包为一个单独的文件

如果公共引用的部分变为了一个单独的文件,再添加上 hash 进行缓存,当再次修改的时候只要更新 hash,这样我们不就能够确定,究竟改动了哪个文件了吗

既然如此,我们一步一步进行探索:

1. 首先使用 CommonsChunkPlugin,提取公共 js

现在我们创建测试入口文件:

src/one.js:

  import jquery from 'jquery';
    console.log('one');

src/two.js:

  import jquery from 'jquery';
    console.log('two');

webpack.config.js

var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].js"
    }
};

执行webpack

输出了2个文件,大小都是 271kb,这是因为 one.js 和 two.js 都引用了 jquery,jquery 打包了2次,分别打包到了两个文件中

这样显然不是很友好,像 jquery 这种文件,显然平时不会改动,还是缓存起来比较好,修改 webpack.config.js

var webpack = require("webpack");
var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].js"
    },
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: "common",
        }),
    ]
};

现在我们添加了 CommonsChunkPlugin 插件,它的作用是提取公共 js,再次执行 webpack

可以看到 one.js 和 two.js 的大小已经不到 1k 了,而 common 则 274k,可以看到 jquery 已经被打包到了 common.js 当中

2. 为文件添加 hash

var webpack = require("webpack");
var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[hash:6].js"
    },
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: "common",
        }),
    ]
};

上面修改了output的输出内容[name].[hash].js

现在执行 webpack:

可以看到打包的三个文件都有了 hash,但需要主意,此时每个文件的 hash 都是一样的

再次执行一遍 webpack:

可以看到,两次构建输出的结果一致,这很好,因为没有修改文件,自然不希望 hash 发生改变

那么接下来,修改一下文件:one.js

import jquery from 'jquery';
console.log('修改one');

悲剧了,所有文件全部修改了 hash,查看输出的结果:

可以发现只修改一个文件,却修改了全部文件的 hash,这个问题很严重,显然不是我们想要的

3. 使用 chunkhash 替代 hash

webpack 中关于缓存,提供了好几种添加 hash 的方法,其中就有 chunkhash,chunkhash 简单来说,就是根据模块内容来添加 hash,既然这样的话,只要文件没有改变,就不会生成新的 hash

var webpack = require("webpack");
var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: "common",
        }),
    ]
};

如上图,修改 filename:[name].[chunkhash:8]/js

执行 webpack

可以看到这一次生成的 hash 是 4897....

但是输出的每个文件的 hash 却不是 4897....

很好,接下来再执行一次 webpack:

可以看到两次输出之间 hash 并没有发生变化

现在,修改 one.js,再执行 webapck

import jquery from 'jquery';
console.log('使用chunkhash后修改one');

可以看到 two.js 的 hash 没有改变 one.js 的 hash 改变了,但 common.js 的 hash 竟然也改了...

4. 提取 manifest

前面用 CommonsChunkPlugin 提取代码后,公共的代码已经被抽离,但是他们之间肯定存在一个映射关系,例如

之所以 commonjs 的 hash 会变,是因为修改 one.js 生成了新的 hash,而 jquery 又与 one.js 存在映射关系,映射关系会更新,也就是说 common.js 它要从新的 one.js 中提取了 jquery,而 manifest 就可以简单理解为模块映射关系的集合,而这个 manifest 将随着这些被分离出来的代码共同打包!

所以现在分离 manifest

var webpack = require("webpack");
var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: "common",
        }),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest' // 用于提取manifest
        })
    ]
};

这里主要是利用 CommonsChunkPlugin 的一个功能,通过默认的名字,来提取公共代码,因为 webpack 打包的是有有一个默认模块就是 manifest,所以我们可以通过这个来实现

现在我们执行 webpack:

可以看到,多输出了一个 manifest.js

接下来,再修改 one.js

import jquery from 'jquery';
console.log('分离manifest后修改one');

可以看到,现在只有 one.js 和 manifest.js 的 hash 发生了改变,common.js 被成功缓存了

使用代码对比工具,比较两次 manifest 之间的区别,可以看到确实是映射的 chunkid 发生了改变

5. 使用 webpack-md5-hash 插件

前面我们输出了一个 manifest.js,但这样还需要单独处理这个 manifest.js,所以可以使用 webpack 的另一个插件 webpack-md5-hash

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
    entry: {
        one: "./src/one.js",
        two: "./src/two.js"
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new WebpackMd5Hash(),
        new webpack.optimize.CommonsChunkPlugin({
            name: "common",
        }),
    ]
};

执行一次打包:

没有 manifest 输出,修改 one.js

import jquery from 'jquery';
console.log('使用WebpackMd5Hash修改one');

再次打包:

这一次仅有 one.js 的 hash 发生了改变

虽然 webpack-md5-hash 解决了我们的问题,但这也让打包的模块关系变成了黑盒,存在一定的未知风险,还需要仔细实践评估是否有问题

6. 打包修改频率超级低的库

前面已经抽离出来了公共代码,但是还存在问题,假如这时候又需要引入 lodash,那 common 的 hash 是否会改变?

修改 one.js

import jquery from 'jquery';
import lodash from 'lodash';
console.log('引入lodash修改one');

修改 two.js

import jquery from 'jquery';
import lodash from 'lodash';
console.log('引入lodash修改two');

这一次,所有文件的 hash 都发生了改变,不仅如此,而且更显著的是 common 的体积增大了

这就意味者 lodash 也被打进了 common 当中,但这本身是一个错误的行为,lodash 和 jquery,平时根本不会对其进行修改,既然如此,那还需要优化,把他们单独打包出去,现在修改 webapack.config.js

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
    entry: {
        two: "./src/two.js",
        one: "./src/one.js",
        common:['jquery','lodash']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new WebpackMd5Hash(),
        new webpack.optimize.CommonsChunkPlugin({
            name: "common",
        }),
    ]
};

这一次在入口处添加了一个 common,common 单独指向了 jquery 和 lodash,这一次我们执行打包

此时,输出的内容没有明显变化,同样是3个文件,大小也完全一致,hash也没有问题

可以看到,common 的大小是 817k

如果这时,再应用了其他的包呢?例如引入 react

修改 one.js

import jquery from 'jquery';
import lodash from 'lodash';
import react from 'react';
console.log('引入react修改one');

修改 two.js

import jquery from 'jquery';
import lodash from 'lodash';
import react from 'react';
console.log('引入react修改one');

执行 webpack

问题来了,common 的大小增加了,很显然react被打包进去了,但如果我们此时,只想永久缓存 jquery 和 lodash 呢,这该怎么办?

修改 webpack.config.js

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
    entry: {
        two: "./src/two.js",
        one: "./src/one.js",
        common:['jquery','lodash']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new WebpackMd5Hash(),
        new webpack.optimize.CommonsChunkPlugin({
            name: 'common',
            minChunks:Infinity
        })
    ]
};

这一次,添加了一句话 minChunks:Infinity

minChunks 属性的可以设置为 2,意思是引用次数为2的模块就抽离出来,而 Infinity 则表示无限,无限就意味着不会有多余的被打包进来

现在执行 webpack 打包

可以看到现在 common 又恢复了 816k,当然 react 也没有抽出来,还在两个文件当中,接下来继续抽离 react

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
    entry: {
        two: "./src/two.js",
        one: "./src/one.js",
        common:['jquery','lodash'],
        react:['react','react-redux']
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: ['react','common'], // 用于提取manifest
            minChunks:Infinity
        }),
        new WebpackMd5Hash(),
    ]
};

通过上面的构建,我们已经将不会改动的类库,单独打包并维持住了 hash。

7. 引入 HashedModuleIdsPlugin 固定模块 id

前面看似完美,但如果我们现在改变一下入口的顺序

entry: {
    react:['react','react-redux'],        
    two: "./src/two.js",
    one: "./src/one.js",
    common:['jquery','lodash'],        
}

可以看到 common 和 react 公共库的 hash 又变了,这是因为,模块 id 是根据 webpack 的解析顺序增量的,如果变换解析顺序,那模块 id 也会随之改变。

所以就需要 HashedModuleIdsPlugin 了,它是根据模块相对路径生成模块标识,如果模块没有改变,那模块标识也不会改变

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
module.exports = {
    entry: {
        common:['jquery','lodash'],                
        react:['react','react-redux'],        
        two: "./src/two.js",
        one: "./src/one.js",
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    plugins:[
        new webpack.optimize.CommonsChunkPlugin({
            name: ['react','common'], // 用于提取manifest
            minChunks:Infinity
        }),
        new webpack.HashedModuleIdsPlugin(),
        new WebpackMd5Hash(),
    ]
};

现在打包后,模块的标识不再是 id 了,而是一个四位的编码了,这样就可以固定住 ip 地址了。

8. 使用 extract-text-webpack-plugin 提取 css 文件

在 src 下创建

one.css:

body{
    color:blue;
}

two.css

h1{
    font-size:24px;
}

修改 one.js 和 two.js 引入 css

import jquery from 'jquery';
import lodash from 'lodash';
import react from 'react';
import './one.css'
console.log('引入css修改one');

修改 webpack.config.js

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
    entry: {
        common: ['jquery', 'lodash'],
        react: ['react', 'react-redux'],
        two: "./src/two.js",
        one: "./src/one.js",
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    fallback: "style-loader",
                    use: "css-loader"
                })
            }
        ]
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['react', 'common'], // 用于提取manifest
            minChunks: Infinity
        }),
        new ExtractTextPlugin("[name].[chunkhash:8].css"),
        new webpack.HashedModuleIdsPlugin(),
        new WebpackMd5Hash()
    ]
};

执行 webpack:

可以看到,成功输出了 js 和 css,但是有点疑问的是,one.css 和 one.js 的 hash 是一样的,这样的话,如果我们改变 one.css 呢?

修改 one.css,再次打包:

发现 css 的 hash 没有任何变化。

接着再修改 one.js,再次打包:

这一次 one.js 和 one.css 的 hash 同时改变了。

9. 使用 contenthash 提取固定 css 的 hash

When using the ExtractTextWebpackPlugin, use [contenthash] to obtain a hash of the extracted file (neither [hash] nor [chunkhash] work).

webpack output 文档种有写,当提取 css 后,用 contenthash 添加 hash

var webpack = require("webpack");
var WebpackMd5Hash = require('webpack-md5-hash');
var path = require('path');
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
    entry: {
        common: ['jquery', 'lodash'],
        react: ['react', 'react-redux'],
        two: "./src/two.js",
        one: "./src/one.js",
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: "[name].[chunkhash:8].js"
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: ExtractTextPlugin.extract({
                    fallback: "style-loader",
                    use: "css-loader"
                })
            }
        ]
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: ['react', 'common'], // 用于提取manifest
            minChunks: Infinity
        }),
        new ExtractTextPlugin("[name].[contenthash:8].css"),
        new webpack.HashedModuleIdsPlugin(),
        new WebpackMd5Hash()
    ]
};

这一次,只是修改了输出的 hash,conenthash 代表的是文本文件内容的 hash 值,也就是只有 style 文件的 hash 值。

执行 webpack:

one.js 和 one.css 的 hash 变的不一样了

接下来,修改 one.css

body{
    color:white;
}

再次执行 webpack:

至此,只有 one.css 发生了变化,准备工作基本就到这里了

四,优化多页打包时间,稳定 hash

1. 约束入口

因为是多页应用,是通过扫入口文件来进行的打包,规则为 js 文件为入口文件,jsx 为引用的资源不被识别为入口

通过 BundleAnalyzerPlugin 插件分析,发现有部分组件被打包为了入口,梳理一遍后,重新打包,打包时间减少了 2/3,当然这是在填以前的坑

生产打包时间是 74578ms

此时压缩和不压缩的打包时间也是3倍的关系:

开发打包时间是 24780ms

好的,围绕这两个时间,我们开始优化

2. 使用 UglifyjsWebpackPlugin 开启多线程打包

首先要做的其实是稳定 hash,但因为生产环境的打包速度太慢,所以我们先优化打包速度,webpack 默认提供的打包是单线程的

const UglifyJSPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
  plugins: [
    new UglifyJSPlugin({
        parallel: true
    })
  ]
}

这个插件是 webpack3 提供的,至于低版本 webapck 的话,需要谨慎处理,不过效果很明显

现在生产打包时间是 51690ms,比之前提速了 1/3

3. 使用 HappyPack 多线程加速 loader

var HappyPack = require('happypack');
var os = require('os');
var happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

...
module: {
        rules: [ {
            test: /\.js[x]?$/,
            exclude: /(node_modules|bower_components)/,
            loader: 'happypack/loader?id=happybabel',
            include: path.join(__dirname, 'static/assets/js')
        }
    }
plugins: [
        new HappyPack({
            id: 'happybabel',
            loaders: ['babel-loader?cacheDirectory=true'],
            threadPool: happyThreadPool,
            cache: true,
            verbose: true
          }),

上面module的rules属性中loader原本事babel-loader,现在将它变成了一个任务,其中有一个id,id对应的就是plugins中的happyPack实例

此时,我们开启了 babel-loader 的多线程模式

现在生产打包时间是 43855ms,比之前又提速了 1/9,这只是 babel-loader,我们还可以为其它的 loader 开启

接着处理 less、css、style 等 loader,这些结合可以一口气搞定

  module: {
        rules: [{
            test: require.resolve('zepto'),
            loader: 'exports-loader?window.Zepto!script-loader'
        }, {
            test: /\.js[x]?$/,
            exclude: /(node_modules|bower_components)/,
            loader: 'happypack/loader?id=happybabel',
            include: path.join(__dirname, 'static/assets/js')
        }, {
            test: /\.less$/,
            use: extractTextPlugin.extract({
                fallback: "style-loader",
                // use: ["css-loader" + (ENV ? '?minimize' : ''), "less-loader", "postcss-loader"]
                use: ["happypack/loader?id=postcss"]
            })
        }]
    }
plugins: [
        new HappyPack({
            id: 'happybabel',
            loaders: ['babel-loader?cacheDirectory=true'],
            threadPool: happyThreadPool,
            // cache: true,
            verbose: true
        }),
        new HappyPack({
            id: 'postcss',
            loaders: ["css-loader" + (ENV ? '?minimize' : ''), "less-loader",'postcss-loader'],
            threadPool: happyThreadPool,
            // cache: true,
            verbose: true
        }),

这样,我们即处理了 babel,同时也搞定了 css,less,postcss 这些 loader

上图 happy[任务名],可以看到打包行为全都开启了多线程,效果显著

现在生产打包时间是 35130ms,此时已经比第一此非优化的时候,提升了一倍的速度

4. 使用 dll 拆分代码

经过前面的过程,想必已经意识到了纯静态得库和组件都需要与打包环节分离开,这就需要dll技术了

dll 技术,其实就是将修改频率低或基本不修改且引用次数多的内容,单独打包

因为设计 dll 后,config 文件的数量剧增,所以需要重新整理目录结构

例如上图,将每一个 webpack 拆分出去,把所有配置文件分离开,例 webpack.dev.js:

var base = require('./webpack.base.js');
var config = {
    entry: require('./dev/entry.js'),
    output: require('./dev/output.js'),
    plugins: require('./dev/plugins.js'),
    devtool: 'eval-source-map'
}
//把配置文件暴露出去;
module.exports = Object.assign(base,config);

基础拆分 webpack 完成后,我们创建一个 webpack.dll.libs.js 用于打包类库

module.exports = {
    libs: [
        'react',
        'react-dom',
        'react-motion',
        'react-redux',
        'redux',
        'axios',
        'prop-types',
        'classnames',
    ]
}

修改 plugins 插件:

var webpack = require('webpack');
var dirVars = require('../common/dir.js');
var path = require('path');
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');//多线程打包
var getDefaultPlugins = require('../common/plugins.js').getDefaultPlugins;
var AssetsPlugin = require('assets-webpack-plugin');//输出映射表

var plugins =[
    new webpack.DllPlugin({
        path: dirVars.dllLibsManiFest,
    }),
    new UglifyJsPlugin({
        parallel: true,
        cache: true
    }),
    new AssetsPlugin({
        filename: 'static/dll/libs-rev-manifest.json'
    }),
]
module.exports = plugins.concat(getDefaultPlugins())

现在执行 webpack

可以看到,只需要1s,就打包了所有的类库,接下来,修改 webpack.prod.js

在 plugins 中添加:

new webpack.DllReferencePlugin({
    manifest: 'static/dll/libs-rev-manifest.json'
}),

此时当我们执行 webpack.prod.js 进行打包,当扫描到 libs 中的打包的内容时,就不会重复打包

4. 开始继续约束 hash

前面已经彻底搞定了打包,但破坏性很大,所以需要系统的验证 hash 是否存在问题

case1:js 改变

修改一个业务代码的 js,添加一句注释,再次打包

可以看到文件 hash 发生了改变,但很不幸,vendor 也发生了改变

解决方案:添加 webpack-md5-hash 插件,使用之后,再次验证,发现 vendorjs 的 hash 不再发生变化

case2:less 改变

只有一个 css 的 hash 发生了变化,没问题

case3: 修改一个入口下自己封装出去的公共方法

上面修改了一个入口内公共使用的 tools 插件,最终是入口的 hash 发生了改变,没问题

case4: 修改公共方法组件 js

主要是多个入口都会引用的组件

测试,只有单独打包出去的 components 的 hash 修改了

case5: 修改公共方法组件 less

只有一个 hash 发生了改变

case6: 添加一个公共组件

只有 components 的 hash 发生了改变

未优化前打包时间 180-200s

优化:

1,约束入口,严格明确入口文件筛选条件后
    生产打包:74578ms
    开发打包:24780ms
2,开启多线程压缩后
    生产打包:51690ms
3,开启多线程编译
    生产打包:35130ms
    开发打包:15031ms
4,拆包
    分解了打包过程,类库4s,组件4s,业务20s,总体30s左右

最终,流程变得可控,打包实现了定制化,hash 得到了保持。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
29 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文