webpck 入门进阶

350 阅读13分钟

webpck 基础配置

安装依赖

  • npm init -y
  • yarn add webpack webpack-cli webpack-dev-server

package.json 中配置

"scripts": {
  "build": "webpack"
},

npm run build 执行webpack命令,先到项目的node_modules\bin\webpack.cmd执行,没有的话去当前设备中找webpack.cmd

webpack 打包配置

新建webpack.config.js文件

entry 单入口

entry: {
   main: './src/index.js'
},
entry: './src/index.js',
entry: ['./src/index.js', './src/index1.js'],
entry: {
  main: ['./src/index.js', './src/index1.js']
},

这4种entry的写法没有区别,数组也不是多入口的配置,最终都打包到一个main.js文件中,所以还是单入口的配置

entry 多入口

entry: {
  index: './src/index.js',
  index1: './src/index1.js'
},
output:{
  path: path.resolve(__dirname, 'dist'),
  filename: '[name].js'
}

entry 有多个key,output的filename 的[name]可以更具entry 的名字自动生成打包文件名称,才是多入口

屏幕快照 2021-08-24 下午5.59.27.png

loader

webpack 只能识别js,json文件,别的文件通过相应的loader将其转化成webpack可识别的文件

  • 处理图片

webpack5之前处理图片要使用file-loader,url-loader,webpack5之后内置了资源,只需配置type就可以访问图片了

 module: {
    rules: [
        // {
        //     test: /\.png$/,
        //     use: [{
        //         loader: 'url-loader',
        //         options: {
        //             name: '[hash:10].[ext]',
        //             esModule: false
        //         }
        //     }]
        // }
        {
            test: /\.png$/,
            type: 'asset/resource'
        }
    ]
},

图片的引入不管是loader 还是 webpack5内置,都有一个问题就是不能在html中直接使用,如果使用需要别的loader 这里指的是src/images 下的图片不能直接在html 中使用

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
    },  //让项目在浏览器,能访问静态文件
 }

配置过这个的话,就可以在html 中直接使用bublic下面的图片,注意引用路径不要带public

 <img src="/01.png" alt="" id="img1">
  • 处理js babel-loader
npm i babel-loader @babel/core @babel/preset-env @babel/preset-react -D
npm install --save-dev @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators

@babel/preset-react react 需要,不是react 就不用安装

babel-loader 调用 @babel/core转化代码,@babel/preset-env告诉 @babel/core转化规则

  • eslint
npm i eslint-loader eslint @babel/eslint-parser @babel/eslint-plugin -D

babel-eslint 被 @babel/eslint-parser 替换了

eslint-loader 弃用了,现在使用eslint-webpack-plugin 插件了 webpack.config.js

{
    test: /\.jsx?$/,
    enforce: 'pre', // 在同类文件中先执行
    exclude: /node_modules/, // 排除文件和asset/resource互斥
    use: [{
        loader: 'eslint-loader',
        options: { fix: true }, // 自动修复打包代码
    }],
},
// 或者
{
     plugins: [
        new ESLintPlugin({
            extensions: /\.jsx?$/,
            exclude: 'node_modules',
            fix: true,
        }),
    ]
}

.eslintrc.js

module.exports = {
    root: true,
    parser: "@babel/eslint-parser",
    parserOptions: {
        requireConfigFile : false,  // 解决项目中创建.eslintrc.js 文件之后第一行代码有红色波浪线的问题
        sourceType: "module",
        ecmaVersion: 2015
    },
    env: {
        browser: true
    },
    plugins: [
        "@babel/eslint-plugin"
    ],
    rules: {
        "indent": "off",
        "no-console": "off"
    }
};

每次手动配置很麻烦,eslint可以继承,使用社区的airbnb规则

npm i eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks  -D

这几个插件之间是有依赖关系的,要统一安装

.eslintrc.js

module.exports = {
    // root: true,
    extends: 'airbnb',
    parser: '@babel/eslint-parser',  // 让eslint的语法也能支持babel的解析语法,是一个允许ESLint在由Babel转换的源代码上运行的解析器。
    parserOptions: {
        requireConfigFile: false, // 解决项目中创建.eslintrc.js 文件之后第一行代码有红色波浪线的问题
        sourceType: 'module',
        ecmaVersion: 2015,
    },
    env: {
        browser: true,  // 浏览器
        node: true,   // node
    },
    plugins: [
        '@babel/eslint-plugin',  // 辅助@babel/eslint-parser,能改变一些内置的规则来更好的支持实验特性
    ],
    rules: {
        indent: 'off',
        'no-console': 'off',
    },
};

配置文件中的rules可以覆盖airbnb规则

文档-资源模块

plugin

扩展webpack的功能

  • html-webpack-plugin 在html模版中自动引入打包后的js文件
plugins:[
    new HtmlWebpackPlugin({
        template: './src/index.html',
        filename: 'index.html'
    })
]
  • clean-webpack-plugin 打包之前清理目标文件目录
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    plugins: [
         new CleanWebpackPlugin({
            cleanOnceBeforeBuildPatterns: ['**/*'],
        }),
    ]
}
  • copy-webpack-plugin copy 文件到指定目录
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    plugins: [
        new CopyWebpackPlugin({
            patterns: [{
                from: path.resolve(__dirname, 'src/copy'),
                to: path.resolve(__dirname, 'dist/copy'),
            }],
        }),
    ]
}
  • mini-css-extract-plugin 生产环境提取css文件

mode

  • development 开启debug工具,打印错误信息,编译速速会快 process.env.NODE_ENV = development
  • production 开启优化,打包结果优化,webpack性能优化 process.env.NODE_ENV = production
  • mode 的默认值是production

设置环境变量

  • --mode --mode的方式process.env.NODE_ENV 在模块文件中可以访问到结果,在webpack.confing.js 这样的配置文件中不能访问到结果 yarn add webpack-dev-server
"scripts": {
    "build": "webpack --mode=production",
    "start": "webpack serve --mode=development"
}

npm start 运行程序,浏览器中打开 屏幕快照 2021-08-24 下午8.20.45.png

  • --env webpack.config.js配置文件以函数接收的方式,能在wenpack配置文件中获取环境变量信息
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = (env) => {
    console.log('----------------env', env, process.env.NODE_ENV)
    // ----------------env { WEBPACK_SERVE: true, development: true } undefined
    return {
        mode: 'development',
        entry: {
            main: './src/index.js'
        },
        output:{
            path: path.resolve(__dirname, 'dist'),
            // filename: '[name].js'
            filename: 'main.js'
        },
        plugins:[
            new HtmlWebpackPlugin({
                template: './src/index.html',
                filename: 'index.html'
            })
        ]
    }
}
 "scripts": {
    "build": "webpack --env=production",
    "start": "webpack serve --env=development"
  }

build 之后模块中的process.env.NODE_ENV不会随着--env改变,依旧是development

  • cross-env 修复差异,让环境变量在模块和配置文件中都可以得到
"scripts": {
    "build": "cross-env NODE_ENV=production webpack",
    "start": "cross-env NODE_ENV=development webpack serve"
},
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')
module.exports = (env) => {
    console.log('------------CONFIG----process.env.NODE_ENV', process.env.NODE_ENV)
    // const isDevelopment = env.development
    return {
        mode: process.env.NODE_ENV,
        entry: {
            main: './src/index.js'
        },
        output:{
            path: path.resolve(__dirname, 'dist'),
            // filename: '[name].js'
            filename: 'main.js'
        },
        plugins:[
            new HtmlWebpackPlugin({
                template: './src/index.html',
                filename: 'index.html'
            }),
            new Webpack.DefinePlugin({
                // process.node_env只是一个常量,可以是任何名词
                'process.node_env': JSON.stringify(process.env.NODE_ENV) 
            })
        ]
    }
}

在index.js 文件中

console.log('-------------------INDEX-----process.env.NODE_ENV-----', process.env.NODE_ENV);
console.log('-----------------INDEX--process.node_env----------', process.node_env);

在webpack.config.js 中

console.log('------------CONFIG----process.env.NODE_ENV', process.env.NODE_ENV)

都能获取到对应的环境变量

  • DefinePlugin
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const Webpack = require('webpack')
module.exports = (env) => {
    console.log('----------------env', env, process.env.NODE_ENV)
    console.log('-----------------webpack--process.node_env----------', process.node_env);
    // ----------------env { WEBPACK_SERVE: true, development: true } undefined
    // -----------------webpack--process.node_env---------- undefined
    const isDevelopment = env.development
    return {
        mode: 'development',
        plugins:[
            new Webpack.DefinePlugin({
                'process.node_env': JSON.stringify(isDevelopment ? 'development' : 'production') 
            })
        ]
    }
}

配置文件获取不到,但是模块文件可以获取到'process.node_env 的值

总结:配置文件中的mode 设定的值,才是在模块文件中真的生效的环境值,但是为了能灵活的在配置文件中配置环境,所以使用cross-env 插件。

devServer

devServer: {
    static: {
        directory: path.join(__dirname, 'public'),
    },  //让项目在浏览器,能访问静态文件
    hot: true, // 热更新
    port: 3200,
    open: false, // 是否自动打开浏览器
    compress: true, // 启动gzip压缩
}

http://localhost:3200/01.png 可以访问到public目录下的图片

devtool

  • 开发 devtool: 'eval-source-map' 这样配置就够了
  • 生产 devtool: 'eval-source-map' 生产环境中最好不要显示出map文件,方式代码泄漏,要是调试,可以手动的在浏览器中配置map做映射

externals

可以引入外部文件,这样就可以减小打包文件的体积 详情

hash,chunckHash,contentHash

  • hash 每次打包都会产生hash,作为文件的标识,防止文件打包之后重复文件名,只要项目里面的某一个文件发生变化,所有的文件hash都要重新生成
  • chunkhash 打包文件之间有相互依赖的关系,index1.js 依赖index2.js 这样的关系就是chunk,webpack打包时,根据chunk产生的hash 就是chunkHash,chunk 发生改变,只有这个模块的hash会发生变化,别的没有依赖的模块不变化
  • contenthash 根据内容产生的hash,内容不变hash不变。比如: index1.js 里依赖 01.css,打包后会产生index1.[contenthash].js 和 01.[contenthash].css, index1.js 里面的内容变化了,01.css 没做改变,再次打包之后只有index1.js 的hash值会变化

hash 也可以写作 fullhash

hash值越精确,打包越慢,contenthash最慢

css 兼容

npm i postcss-loader postcss-preset-env -D

webpack.config.js

 {
    test: /\.css$/,
    exclude: /node_modules/,
    use: ['style-loader', 'css-loader', 'postcss-loader'],
},

postcss.config.js

module.exports = {
    plugins: [
      [
        'postcss-preset-env',
        {
          // Options
          browsers: 'last 5 versions',
        },
      ],
    ],
};

html css js 压缩

html5 中mode=production的时候,会自动压缩

px 转化rem

npm i px2rem-loader lib-flexible -D

在全局共用模块中import 'lib-flexible'

webpack.config.js

{
    test: /\.css$/,
    exclude: /node_modules/,
    use: [
        'style-loader',
        'css-loader',
        'postcss-loader',
        {
            loader: 'px2rem-loader',
            // options here
            options: {
                remUni: 75,
                remPrecision: 8,
            },
        },
    ],
},

polyfill

babel在转化js的时候,只能转化当前进度是stage4的语法,小于4的不能转化,而有的浏览器平台,不能支持我们,在代码中所写的新的语法和api,所以需要polyfill帮助我们抹平平台差异

@babel/polyfill

npm install --save @babel/polyfill

browsers

.browserslistrc

last 50 version
> 1%
not dead

polyfill 的抹平依据browserslistrc的指定规则,如果不配置,打包会报错

@babel/preset-env

babel.config.json

{
    "presets": ["@babel/preset-env"], // 从前向后执行
    "plugins": []
}

polyfill 当前依据browserlistrc和babel.config.json中的@babel/preset-env可以在打包的时候处理js中新的语法问题。如() => {}箭头函数,但是class, Promise这样的新api还是不能处理

为了处理这种不同平台的差异可以在"@babel/preset-env中添加useBuiltIns 选项

  • useBuiltIns: false

不管.browserslistrc 配置的规则把所有的抹平文件全部引入,文件的体积会很大

并且需要手动在入口文件import '@babel/polyfill';

  • useBuiltIns: entry "corejs": 2
"presets": [
    [
        "@babel/preset-env",
        {
            "useBuiltIns": "entry",
            "corejs": 2
        }
    ],
   ], // 从前向后执行

会根据浏览器的差异,加入补丁,文件体积依据配置的打包规则而定,但是还是需要手动在入口文件

import '@babel/polyfill';

"corejs": 3 (当前最新的用法)

npm i core-js@3 regenerator-runtime --save
"presets": [
    [
        "@babel/preset-env",
        {
            "useBuiltIns": "entry",
            "corejs": 3
        }
    ],
  ]

需要手动在入口文件

import 'core-js/stable';
import 'regenerator-runtime/runtime';
  • useBuiltIns: usage
 "presets": [
        [
            "@babel/preset-env",
            {
                "useBuiltIns": "usage",
                "corejs": 3
            }
        ],
    ]

会根据浏览器的差异,加入补丁,文件体积会明显变小,且不用手动引入@babel/polyfill或者import 'core-js/stable';import 'regenerator-runtime/runtime';

entry 在入口文件处引入就会挂载到全局变量上,usage会在打包的时候,在每一个有语法抹平的模块中增加抹平代码的代码块,类似reqire('XXX')

适合项目中使用

@babel/plugin-transform-runtime 解决全局污染

"@babel/preset-env"useBuiltInsusage会每次重复引入依赖,entry会污染全局环境,且2种方式对应的corejs还需要手动安装包。使用@babel/plugin-transform-runtime插件可以自动导入所需依赖,减少开发者的配置,不污染全局变量,且效果和usage十分相似。

npm i @babel/plugin-transform-runtime --save

babel.config.json

{
    "presets": [
        [
            "@babel/preset-env"
            // 可以被下面的"@babel/plugin-transform-runtime"配置替代
            // {
            //     "useBuiltIns": "entry",
            //     "corejs": 3
            // }
        ],
        "@babel/preset-react"], // 从前向后执行
    "plugins": [  // 从后向前执行
        [
            "@babel/plugin-transform-runtime",
            {
              "corejs": 3,
              "helpers": true,
              "regenerator": true
            }
        ],
    ]
}

适合组件库或者插件里使用

webpack 高级进阶

AST语法树

javascript parser 可以把代码转化为ast语法树,ast语法树定义了代码结构,通过这颗树,可以精准定位到声明语句,赋值语句,运算语句,可以对代码,分析,优化,变更。

yarn add esprima estraverse escodegen -D
  • esprima 把代码解析成ast
  • estraverse 遍历语法树
  • escodegen 把ast树再转换成源代码

ast的语法遍历是深度遍历,并且会跳过没有type的节点

ast 转换工具

function ast(){}

屏幕快照 2021-08-30 上午11.32.32.png

babel 转化箭头函数

const core = require('@babel/core')
const types = require('babel-types')
let arrowFunction = require('babel-plugin-transform-es2015-arrow-functions')

let codeEs6 = `
    const sum = (a, b) => {
        return a+b
    }
`

let arrowFunctionBySelf = {
    visitor: {
        ArrowFunctionExpression(nodePath) {
            let node = nodePath.node
            node.type = 'FunctionExpression'
        }
    }
}

let codeEs5 = core.transform(codeEs6, {
    plugins: [arrowFunctionBySelf]
})
// 箭头函数的最终转化结果
console.log('-----------------------------', codeEs5.code);
  • 箭头函数的ast

屏幕快照 2021-08-30 下午2.38.48.png

  • 非箭头函数的ast

屏幕快照 2021-08-30 下午2.40.03.png

可以看到箭头函数和普通函数的差别是init时候的node的type类型,在遇到箭头函数ArrowFunctionExpression时把node的type转化成普通函数的类型FunctionExpression,就可以了。

  • class 转化 构造函数
const core = require('@babel/core')
const t = require('babel-types')

const codeEs6 = `
class Person {
    constructor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
}
`
const classPlugin = {
    visitor: {
        ClassDeclaration(nodePath) {
            const {
                node
            } = nodePath
            const id = node.id
            // constructor的kind和getName的kind 是不一样的
            const methods = node.body.body
            let nodes = []
            methods.forEach((item) => {
                if (item.kind === 'constructor') {
                    // 创建constructor ast树转化之后的函数
                    const constructorFun = t.functionDeclaration(id, item.params, item.body, item.generator, item.async);
                    nodes.push(constructorFun)
                } else {
                    // 转化getName函数转化之后的函数
                    const memberExpressionPrototype = t.memberExpression(id, t.identifier('prototype'));
                    // 左边
                    const leftMember = t.memberExpression(memberExpressionPrototype, item.key);
                    // 成员函数(右边)
                    const functionExpression = t.functionExpression(id, item.params, item.body, item.generator, item.async);
                    // 整个函数
                    const assignmentExpression = t.assignmentExpression('=', leftMember, functionExpression);
                    nodes.push(assignmentExpression)
                }
            })

            if (nodes.length === 1) {
                // 只有构造函数
                nodePath.replaceWith(nodes[0])
            } else {
                nodePath.replaceWithMultiple(nodes)
            }
        }
    }
}

const codeEs5 = core.transform(codeEs6, {
    plugins: [classPlugin]
})

console.log('-------------codeEs5----------------', codeEs5.code);

webpack 工作流程

  1. 获取配置文件和shell(npm run XXX --params)参数合并
  2. 依据参数产生compiler编译对象
  3. 加载所有的配置插件
  4. 执行compiler的run方法
  5. 根据配置中的entry找出入口文件
  6. 从入口文件调用所有的loader对模块进行编译
  7. 找到所有本地模块依赖的模块,直到所有入口依赖的模块都处理过
  8. 根据入口和模块之间的依赖关系,组成包含多个模块的chunk
  9. 把chunk转换成文件输出

webpack.js

let Compiler = require('./Compiler')
function webpack(options) {
    // 初始化配置参数
    const shellOPtions = process.argv.slice(2).reduce((config, args) => {
        let [key, value] = args.split('=')
        config[key.slice(2)] = value
        return config
    }, {})
    let optionsAll = {
        ...shellOPtions,
        options
    }
    const compiler = new Compiler(optionsAll)
    let plugins = options.plugins || []
    if (plugins && Array.isArray(plugins)) {
        plugins.forEach((plugin) => {
            plugin.apply(compiler)
        })
    }
    return compiler
}

module.exports = webpack;

complier.js

class Compiler {
    constructor(options) {
        // console.log('-----------------------------', options);
    }
    run() {

    }
}
module.exports = Compiler
const webpack = require('./webpack');
const options = require('./webpack.config')

let compiler = webpack(options)

屏幕快照 2021-09-01 上午9.22.21.png

webpack 热加载流程

  • 创建compiler编译对象
  • 利用HotModuleReplacementPlugin插件生成对应的hash结果文件
  • 创建server服务器,为entry添加别的客户端和服务端文件
  • 创建express实例,接受创建的中间件,通过compiler的watch监听文件的变化,每次文件变化,就重新编译,并且把编译结果放在内存里面
  • 创建websocket服务器,告诉客户端变化之后产生的hash和ok,主要通过webpack-dev-server 调用 webpack api 监听 compile的 done 事件

webpackHmr/node_modules/webpack-dev-server/lib/Server.js

setupHooks() {
    const addHooks = (compiler) => {
      compiler.hooks.invalid.tap("webpack-dev-server", () => {
        if (this.webSocketServer) {
          this.sendMessage(this.webSocketServer.clients, "invalid");
        }
      });
      compiler.hooks.done.tap("webpack-dev-server", (stats) => {
        if (this.webSocketServer) {
          this.sendStats(this.webSocketServer.clients, this.getStats(stats));
        }
        this.stats = stats;
      });
    };

    if (this.compiler.compilers) {
      this.compiler.compilers.forEach(addHooks);
    } else {
      addHooks(this.compiler);
    }
  }
  • 客户端接收到服务端发出的消息,根据type类型作出响应,type是hash就把hash存起来,是ok就执行reload操作
  • 客户端根据hash向服务端请求是不是要更新
  • 服务端接收到请求,更新代码 详情

loader

loader本质上是一个函数,接收资源,为资源代码处理,返回处理结果,下一个loader接着处理上一个loader处理之后的结果。

loader 有4种 pre normal inline post 通过enfore配置。

loader 的内部有一个pitch的属性函数,loader真正执行的时候,是按照先从前向后运行pitch 函数,没有pitch 或者pitch没有返回值,就处理下一个loader的pitch,当loader的pitch处理完成之后,在从后向前处理loader的函数实体。如果loader的pitch有返回值,就从当前loader开始向前处理loader实体。

loader 中相关的api在loader-runner

简单的file-loader

loader-utils中有获取loader参数的api

const {
    getOptions,
    interpolateName
} = require('loader-utils')

function loader(content) {
    let options = getOptions(this) || {}
    // hash
    let filename = interpolateName(this, options.filename, {
        content
    })
    this.emitFile(filename, content)
    return `module.exports=${JSON.stringify(filename)}`
}
loader.raw = true
module.exports = loader;

plugin

webpack 的插件把自己的方法,放在webpack 的钩子上,在webapck的编译过程中,触发自己的插件方法

插件是一个类或者构造函数,在原型上有一个apply方法 tapable包是实现插件的主要工具

webpack配置优化

  • estensions
resolve: {
    extensions: ['.js', '.json'],
  },
  • 配置别名加快查找速度,不需要从node_modules中按模块规则查找
resolve: {
    alias: {
      Utilities: path.resolve(__dirname, 'src/utilities/'),
      Templates: path.resolve(__dirname, 'src/templates/'),
    },
  }
  • modules 默认node_modules,加上配置之后,会先查找配置的文件,找到之后,就不继续向下查找了
resolve: {
    modules: [path.resolve(__dirname, 'xxx'), 'node_modules'],
  }
  • noParse 对于没有使用import,require,define的大型包,可以省略解析步骤
module: {
    noParse: (content) => /jquery|lodash/.test(content),
  },
  • webpack.IgnorePlugin 去除包里面无用的内容,减小包的体积
 new webpack.IgnorePlugin({ resourceRegExp: /xxx/, contextRegExp: [moduleName] });
  • speed-measure-webpack-plugin 打包速度分析插件
  • webpack-bundle-analyzer 生成代码分析报告插件
  • purgecss-webpack-plugin 可以去除未使用的css,必须和mini-css-extract-plugin一起使用
  • thread-loader 放置在这个loader之后的loader会在单独的worker池中运行,但是每个worker都是一个独立的node.js进程,它有大约600ms的开销。还有进程间通信的开销。 只在昂贵的操作中使用这个加载器!

import和commonjs在打包过程中的不同点

  • commonjs调用commonjs: commonjs模块原样输出
  • commonjs调用es6模块: 会在exports上添加__esModule:true
  • es6调用es6: 未被使用的模块会在压缩的时候去除
  • es6调用commonjs: commonjs模块不会被webpack编译,commonjs模块没有default属性

dev-server如何启动的

  1. 启动一个http服务
  2. webpack构建的时候输出bundle到内存,http服务器从内存中读取bundle文件
  3. 监听文件变化,之后重新执行第二步

webpack数据持久化缓存实现

  • 服务服端设置缓存头
  • 打包依赖和运行时到不同的chunk中(splitChunk)
  • 延迟加载,使用import()的方式可以动态加载到文件分到独立的chunk,得到chunkHash
  • 保证hash值稳定,编译的文件内容的hash,尽量不影响其他的文件的hash

webpack遇到import会发生什么

import的设计思想是,在遇到import不立即执行,还是产生一个引用,等到需要使用的时候,再去模块里面取值

webpack在遇到import的时候,会把import转化成commonjs,然后将node_module里面的依赖打包成自执行函数

软件优化

  • html优化,减少空格,减少table,不用iframe
  • css优化,能使用css的尽量不使用js,用transform替代position,left,top会重绘和重排
  • js图片优化,懒加载,延迟加载。视频音频在使用的时候才加载,不要使用闭包
  • http:尽量减少
  • 利用浏览器的缓存,把不常更新的静态资源做缓存处理(304)