前端模塊化(webpack)

前言

前端模塊化是一種開(kāi)發(fā)管理規(guī)范,前端開(kāi)發(fā)發(fā)展到現(xiàn)在,已經(jīng)有很多成熟的構(gòu)建工具可以幫助我們完成模塊化的開(kāi)發(fā)需求,但我們?nèi)孕枰钊胩骄恳幌略懒矗@些模塊化構(gòu)建工具到底幫助我們做了哪些事情,這要我們才能更好的利用它們劲件,從而提高我們的開(kāi)發(fā)效率掸哑,本篇我們將以 webpack 為例约急,進(jìn)行分析。

webpack 究竟解決了什么問(wèn)題

如何在前端項(xiàng)目中更高效的管理和維護(hù)項(xiàng)目中的每一個(gè)資源

  • 模塊化的演化進(jìn)程

    • Stage 1 - 文件劃分方式

      • 好處:提高了代碼復(fù)用性苗分,代碼可抽離厌蔽,可維護(hù),方便模塊間組合分解摔癣。
      • 弊端:所有 JS 文件共用全局作用域奴饮,會(huì)有命名沖突,污染全局環(huán)境择浊;
        沒(méi)有私有的模塊空間戴卜,可以在外面任意修改。
      // a.js
      var a = "hello a";
      console.log(a);
      // b.js
      var a = "hello b";
      console.log(a);
      
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <title>Document</title>
        </head>
        <body>
          <script src="a.js"></script>
          <script src="b.js"></script>
        </body>
      </html>
      
    • Stage 2 - 命名空間方式

      • 好處:解決了命名沖突問(wèn)題琢岩。
      • 弊端:模塊成員依然可以被修改投剥。
      // module-a.js
      window.moduleA = {
        var a = 'hello a'
        console.log(a)
      }
      // module-b.js
      window.moduleB = {
        var a = 'hello b'
        console.log(b)
      }
      
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <title>Document</title>
        </head>
        <body>
          <script src="module-a.js"></script>
          <script src="module-b.js"></script>
        </body>
      </html>
      
    • Stage 3 - IIFE 依賴參數(shù)

      • 好處:解決了命名沖突問(wèn)題,全局作用域的問(wèn)題担孔,模塊依賴
      • 弊端:模塊加載順序江锨,文件數(shù)量過(guò)多
      // module-a.js
      ;(function(){
        window.moduleA = {
         var name = 'module-a';
         console.log(name)
        }
      })()
      // module-b.js
      ;(function(){
        window.moduleB = {
         var name = 'module-b';
         console.log(name)
        }
      })()
      
  • 由模塊化產(chǎn)生的規(guī)范

    • CommonJS、AMD糕篇、 ESModules 規(guī)范

      // CommonJS 服務(wù)端規(guī)范(node環(huán)境)
      // lib.js
      var counter = 3;
      function incCounter() {
        counter++;
      }
      module.exports = {
        counter,
        incCounter,
      };
      // main.js
      var counter = require("./lib").counter;
      var incCounter = require("./lib").incCounter;
      
      console.log(counter); // 3
      incCounter();
      console.log(counter); // 3
      
      //AMD規(guī)范來(lái)源于 require.js
      // 使用步驟
      // 1. index.html中引入(require.js):https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js(cdn)
      // 2. script中設(shè)置啄育,amd.js 是自己的代碼文件
      // <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js" data-main="./amd.js"></script>
      // 3. 代碼示例
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js" data-main="./amd.js"></script>
        <title>Document</title>
      </head>
      <body>
      </body>
      </html>
      
      // amd.js
      requirejs.config({
        baseUrl: './',
        paths: {
          app: './app'
        }
      });
      requirejs(['app/main']);
      
      // app/main.js
      define(function (require) {
        var messages = require('./messages');
        console.log(messages.getHello());
      });
      
      // app/messages.js
      define(function () {
        return {
            getHello: function () {
              return 'Hello World';
            }
        };
      });
      
      // ESModules 瀏覽器環(huán)境
      <!DOCTYPE html>
      <html lang="en">
      <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
      </head>
      <body>
        <script type="module" src="./app.js"></script>
      </body>
      </html>
      
      // app.js
      import { name, age } from './module.js'
      console.log(name, age);
      
      // module.js
      const name = 'xinmin'
      const age = 18
      
      export { name, age }
      
    • 總結(jié):

      我們所使用的 ES Modules 模塊系統(tǒng)本身就存在環(huán)境兼容問(wèn)題。盡管現(xiàn)如今主流瀏覽器的最新版本都支持這一特性拌消,但是目前還無(wú)法保證用戶的瀏覽器使用情況挑豌。所以我們還需要解決兼容問(wèn)題。隨著前端業(yè)務(wù)復(fù)雜度的增加墩崩,開(kāi)發(fā)過(guò)程中浮毯,模塊化是必須的,所以我們需要引入工具來(lái)解決模塊化所帶來(lái)的兼容性問(wèn)題泰鸡。因此,各類(lèi)如 webpack壳鹤、gulp盛龄、vite 等打包工具就產(chǎn)生了。

      ES Modules 采用的是編譯時(shí)就會(huì)確定模塊依賴關(guān)系的方式芳誓。

      CommonJS 的模塊規(guī)范中余舶,Node 在對(duì) JS 文件進(jìn)行編譯的過(guò)程中,會(huì)對(duì)文件中的內(nèi)容進(jìn)行頭尾包裝锹淌,在頭部添加(function(export, require, modules, __filename, __dirname){\nxxxxxx\n})

  • 更為理想的方式

    1. 在頁(yè)面中引入一個(gè) js 入口文件匿值,其余用到的模塊通過(guò)代碼控制,按需加載
    2. 同時(shí)在編碼代碼的過(guò)程中有著相應(yīng)的約束規(guī)范保證所有的開(kāi)發(fā)者實(shí)現(xiàn)一致
  • 引申出兩點(diǎn)需求

    1. 一個(gè)統(tǒng)一的模塊化標(biāo)準(zhǔn)規(guī)范
    2. 一個(gè)可以自動(dòng)加載模塊的基礎(chǔ)庫(kù)

如何使用 webpack 實(shí)現(xiàn)模塊化打包

本質(zhì)上赂摆,webpack 是一個(gè)用于現(xiàn)代 JavaScript 應(yīng)用程序的 靜態(tài)模塊打包工具挟憔。當(dāng) webpack 處理應(yīng)用程序時(shí)钟些,它會(huì)在內(nèi)部從一個(gè)或多個(gè)入口點(diǎn)構(gòu)建一個(gè) 依賴圖(dependency graph),然后將你項(xiàng)目中所需的每一個(gè)模塊組合成一個(gè)或多個(gè) bundles绊谭,它們均為靜態(tài)資源政恍,用于展示你的內(nèi)容。

  • 核心概念

    入口(entery):指示 webpack 應(yīng)該使用哪個(gè)模塊來(lái)作為構(gòu)建內(nèi)部依賴圖的開(kāi)始达传,可以配置單入口或者多入口

    // 單個(gè)入口(簡(jiǎn)單)寫(xiě)法
    const config = {
      entry: "./path/to/my/entry/file.js",
    };
    // 單個(gè)入口篙耗,對(duì)象寫(xiě)法
    const config = {
      entry: {
        main: "./path/to/my/entry/file.js",
      },
    };
    // 多頁(yè)面應(yīng)用
    const config = {
      entry: {
        pageOne: "./src/pageOne/index.js",
        pageTwo: "./src/pageTwo/index.js",
        pageThree: "./src/pageThree/index.js",
      },
    };
    

    輸出(output):指定打包輸出文件路徑與名稱(chēng)

    // 基礎(chǔ)使用
    const config = {
      output: {
        filename: 'bundle.js',
        path: '/home/proj/public/assets'
      }
    };
    // 多入口起點(diǎn)(使用占位符)
    const config = {
      entry: {
        app: './src/app.js',
        search: './src/search.js'
      },
      output: {
        filename: '[name].js',
        path: __dirname + '/dist'
      }
    }
    // 使用cdn和資源hash
    output: {
      path: "/home/proj/cdn/assets/[hash]",
      publicPath: "http://cdn.example.com/assets/[hash]/"
    }
    

    Module:模塊,在 webpack 里一切皆模塊,一個(gè)模塊對(duì)應(yīng)著一個(gè)文件。webpack 會(huì)從配置的 Entry 開(kāi)始遞歸找出所有依賴的模塊宪赶。

    Chunk:代碼塊,一個(gè) Chunk 由多個(gè)模塊組合而成宗弯,用于代碼合并與分割。

    loader:loader 用于對(duì)模塊的源代碼進(jìn)行轉(zhuǎn)換(安裝相應(yīng)處理的 loader)

    • 三種使用方式(配置(推薦)搂妻、內(nèi)聯(lián)蒙保、CLI)
    // 配置
    module.exports = {
      module: {
        rules: [
          {
            test: /\.css$/,
            use: [
              { loader: 'style-loader' },
              {
                loader: 'css-loader',
                options: {
                  modules: true
                }
              }
            ]
          },
          { test: /\.ts$/, use: 'ts-loader' }
        ]
      }
    };
    // 內(nèi)聯(lián)
    import Styles from 'style-loader!css-loader?modules!./styles.css';
    // CLI
    webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'
    

    插件(plugin):插件目的在于解決 loader 無(wú)法實(shí)現(xiàn)的其他事

    const HtmlWebpackPlugin = require("html-webpack-plugin"); //通過(guò) npm 安裝
    const webpack = require("webpack"); //訪問(wèn)內(nèi)置的插件
    const path = require("path");
    
    const config = {
      entry: "./path/to/my/entry/file.js",
      output: {
        filename: "my-first-webpack.bundle.js",
        path: path.resolve(__dirname, "dist"),
      },
      module: {
        rules: [
          {
            test: /\.(js|jsx)$/,
            use: "babel-loader",
          },
        ],
      },
      plugins: [
        new webpack.optimize.UglifyJsPlugin(),
        new HtmlWebpackPlugin({ template: "./src/index.html" }),
      ],
    };
    
    module.exports = config;
    

    模式(mode):根據(jù)開(kāi)發(fā)和生產(chǎn)環(huán)境加載不同的插件進(jìn)行處理

  • <b> webpack 的構(gòu)建流程 </b>

    webpack 打包的執(zhí)行流程

    • 在 webpack 函數(shù)中如傳入配置信息叽讳,返回 compiler 實(shí)例
    • 調(diào)用 compiler 實(shí)例的 run 方法進(jìn)行編譯

    插件處理

    • 插件是在 complier 創(chuàng)建之后完成掛載的追他,但是掛在不意味著執(zhí)行、
    • 某些插件是在整個(gè)流程的某些時(shí)間點(diǎn)上觸發(fā)的岛蚤,所以這種情況就要是使用到鉤子 tapable
    • 插件其實(shí)就是一個(gè)具有 apply 函數(shù)的類(lèi)

    處理入口

    • 從配置文件中讀取 entry 的值邑狸,內(nèi)部轉(zhuǎn)化為對(duì)象進(jìn)行處理

    新增屬性

    • 整個(gè)打包結(jié)束之后,會(huì)產(chǎn)生出很多的內(nèi)容涤妒,這些內(nèi)容需要存儲(chǔ)

    初始化編譯

    • 定位入口文件的絕對(duì)路徑
    • 統(tǒng)一路徑分隔符
    • 調(diào)用自己的方法來(lái)實(shí)現(xiàn)編譯

    loader 參與打包工作

    • 讀取被打包模塊的源文件
    • 使用 loader 來(lái)處理源文件(依賴的模塊)
    • loader 就是一個(gè)函數(shù)单雾,接受原始數(shù)據(jù),處理之后返回給 webpack 繼續(xù)使用
    • 以降序的方式的方式來(lái)執(zhí)行 loader

    模塊編譯實(shí)現(xiàn)(單模塊)

    • webpack 找到 a.js 模塊之后她紫,就是對(duì)它進(jìn)行處理硅堆,處理之后的內(nèi)容就是一個(gè)鍵值對(duì)
    • 鍵:./src/a.js,而值就是 a.js 的源代碼
      1. 獲取被打包模塊的模塊 id

    ast 語(yǔ)法樹(shù)贿讹,實(shí)現(xiàn) ast 遍歷(webpack 中解析使用 acorn)

    • @babel/parser 解析器渐逃,將源代碼轉(zhuǎn)化成 ast 語(yǔ)法樹(shù)
    • @babel/traverse 實(shí)現(xiàn) ast 語(yǔ)法樹(shù)遍歷
    • @babel/generator 將處理后 ast 轉(zhuǎn)換成可執(zhí)行的源代碼
    • @babel/core 和 @babel/preset-env 將 AST 語(yǔ)法樹(shù)轉(zhuǎn)換為瀏覽器可執(zhí)行代碼
  • 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 loader

    const marked = require("marked");
    module.exports = (source) => {
      const html = marked.parse(source);
      const code = `module.exports = ${JSON.stringify(html)}`;
      // const code = `exports =${JSON.stringify(html)}`
      // const code = `export default = ${JSON.stringify(html)}`
      return code;
    };
    
  • 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 plugin

    // 去除開(kāi)發(fā)環(huán)境打包中多余的注釋
    class RemoveCommentsPlugin {
      apply(compiler) {
        compiler.hooks.emit.tap("RemoveCommentsPlugin", (compilation) => {
          // compilation 可以理解為此次打包的上下文
          for (const name in compilation.assets) {
            console.log("compilation", compilation.assets[name].source());
            if (name.endsWith("js")) {
              const contents = compilation.assets[name].source();
              const noComments = contents.replace(/\/\*{2,}\/\s?/g, "");
              compilation.assets[name] = {
                source: () => noComments,
                size: () => noComments.length,
              };
            }
          }
        });
      }
    }
    module.exports = RemoveCommentsPlugin;
    
  • 實(shí)現(xiàn)一個(gè) min-pack

    const parser = require("@babel/parser");
    const traverse = require("@babel/traverse").default;
    const babel = require("@babel/core");
    const { SyncHook } = require("tapable");
    const fs = require("fs");
    const path = require("path");
    
    class Compiler {
      constructor(options) {
        this.options = options;
        // this.entries = new Set(); // 保存打包過(guò)程中的入口信息  webpack4中是數(shù)組
        this.modules = []; // 保存打包過(guò)程中出現(xiàn)的module信息
        // this.chunks = new Set(); // 保存代碼塊信息
        // this.files = new Set(); // 保存所有產(chǎn)出文件的名稱(chēng)
        this.assets = {}; // 資源清單
        this.context = this.options.context || process.cwd();
        this.hooks = {
          entryInit: new SyncHook(["compilation"]),
          beforeCompile: new SyncHook(),
          afterCompile: new SyncHook(),
          afterPlugins: new SyncHook(),
          emit: new SyncHook(),
          afterEmit: new SyncHook(),
        };
      }
    
      // 構(gòu)建啟動(dòng)
      run() {
        // 執(zhí)行 plugins
        // this.hooks.entryInit.call(this.assets);
    
        /// 1. 確定入口信息
        let entry = {};
        if (typeof this.options.entry === "string") {
          entry.main = this.options.entry;
        } else {
          entry = this.options.entry;
        }
    
        /// 2. 確定入口文件的絕對(duì)路徑
        for (let entryName in entry) {
          // TODO: 調(diào)用自定義的方法來(lái)實(shí)現(xiàn)具體的編譯過(guò)程,得到結(jié)果
          const entryModule = this.build(entry[entryName]);
    
          // 添加到module中
          this.modules.push(entryModule);
        }
    
        /// 3. 遞歸調(diào)用獲取所有依賴內(nèi)容
        this.modules.forEach(({ dependecies }) => {
          if (Object.keys(dependecies).length > 0) {
            Object.keys(dependecies).forEach((deps) => {
              this.modules.push(this.build(dependecies[deps]));
            });
          }
        });
    
        /// 4. 生成依賴關(guān)系圖
        const dependencyGraph = this.modules.reduce(
          (graph, item) => ({
            ...graph,
            [item.filename]: {
              dependecies: item.dependecies,
              code: item.code,
            },
          }),
          {}
        );
        // console.log('dependencyGraph', dependencyGraph);
        this.assets = dependencyGraph;
        // console.log('this.assets', this.assets)
        // 執(zhí)行 plugins
        this.hooks.entryInit.call(this.assets);
        /// 5. 生成 bundle
        this.generate(dependencyGraph);
      }
    
      // 獲取ast
      getAst(filePath) {
        let code = fs.readFileSync(filePath, "utf-8");
        let loaders = [];
        // console.log('filePath', filePath);
        const rules = this.options.module?.rules;
        for (let i = 0; i < rules?.length; i++) {
          // 從眾多的 rules 當(dāng)中找到 匹配的文件的配置
          if (rules[i].test.test(filePath)) {
            loaders = [...loaders, ...rules[i].use];
          }
        }
        //* 調(diào)用loader
        for (let i = loaders.length - 1; i >= 0; i--) {
          let abPath = loaders?.[i];
          if (loaders[i]?.includes("./")) {
            abPath = path.resolve(this.context, loaders[i]);
          }
          code = require(abPath)(code);
        }
        const ast = parser.parse(code, { sourceType: "module" });
        return ast;
      }
      // 獲取依賴關(guān)系
      getDependecies(ast, fileName) {
        const dependencies = {};
        traverse(ast, {
          CallExpression: (nodePath) => {
            const dirname = path.dirname(fileName);
            const node = nodePath.node;
            // 在ast中找到了require
            if (node.callee.name === "require") {
              const rPath = node.arguments[0].value;
              // 獲取相對(duì)路徑
              const aPath = path.resolve(dirname, rPath);
              dependencies[rPath] = aPath;
            }
          },
          // 在ast中找到import
          ImportDeclaration: (nodePath) => {
            const dirname = path.dirname(fileName);
            const rPath = nodePath.node.source.value;
            const aPath = path.resolve(dirname, rPath);
            dependencies[rPath] = aPath;
          },
        });
        return dependencies;
      }
    
      // 編譯ast
      getTranslateCode(ast) {
        const { code } = babel.transformFromAst(ast, null, {
          presets: ["@babel/preset-env"],
        });
        return code;
      }
    
      // 編譯
      build(filename) {
        const ast = this.getAst(filename);
        const dependecies = this.getDependecies(ast, filename);
        const code = this.getTranslateCode(ast);
        return {
          filename,
          dependecies,
          code,
        };
      }
    
      // 生成
      generate(code) {
        const filePath = path.join(this.options.output.path, "main.js");
        const bundle = `(function(graph){
          function require(moduleId){ 
            function localRequire(relativePath){
              return require(graph[moduleId].dependecies[relativePath])
            }
            var exports = {};
            (function(require,exports,code){
              eval(code)
            })(localRequire,exports,graph[moduleId]?.code);
            return exports;
          }
          require('${this.options.entry}')
        })(${JSON.stringify(code)})`;
    
        // console.log('filePath', filePath, bundle);
        fs.writeFileSync(filePath, bundle, "utf-8");
        // try {
        //   fs.writeFileSync(filePath, bundle, "utf-8")
        // } catch (e) {
        //   fs.mkdirSync(path.dirname(filePath))
        //   fs.writeFileSync(filePath, bundle, "utf-8")
        // }
      }
    }
    
    module.exports = Compiler;
    
  • 配合 min-pack 實(shí)現(xiàn) css-loader

    module.exports = (source) => {
      // console.log('source', source);
      let str = `
      let style = document.createElement("style");
      style.innerHTML = ${JSON.stringify(source)};
      document.head.appendChild(style);
      `;
      return str;
    };
    
  • 配合 min-pack 實(shí)現(xiàn) DemoPlugin

    class DemoPlugin {
      apply(compiler) {
        compiler.hooks.entryInit.tap("DemoPlugin", (compilation) => {
          if (
            Array.isArray(Object.keys(compilation)) &&
            Object.keys(compilation).length > 0
          ) {
            for (let k in compilation) {
              if (k.endsWith("b.js")) {
                compilation[k].code =
                  compilation[k].code + `console.log('min-webpack v1.1')`;
    
                console.log(" compilation[k].code");
              }
            }
          }
          // compilation 可以理解為此次打包的上下文
          return compilation;
        });
      }
    }
    
    module.exports = DemoPlugin;
    

webpack 的性能優(yōu)化

優(yōu)化方向:構(gòu)建性能民褂、傳輸性能茄菊、運(yùn)行性能

  • 構(gòu)建性能

    • 優(yōu)化開(kāi)發(fā)體驗(yàn)

      1. 自動(dòng)更新:watch,webpack-dev-server赊堪,webpack-dev-middleware
      2. 熱更新:@pmmmwh/react-refresh-webpack-plugin react-refresh
    • 加快編譯速度

      1. 使用最新 node面殖,npm,webpack 版本哭廉,有助于提升性能
      2. cache:提升二次構(gòu)建速度脊僚,緩存 webpack 模版和 chunk(webpack5)
      3. 減少非必要 loader、plugins 的使用遵绰,都會(huì)增加編譯時(shí)間
      4. 使用 loader 時(shí)辽幌,配置 rule.exclude:排除模塊范圍增淹,減少 loader 的應(yīng)用范圍
      5. 使用 webpack 資源模塊代替原來(lái)的 assets loader(如:file-loader/url-loader)(webpack5)
      6. 優(yōu)化 resolve 配置(配置別名,根據(jù)項(xiàng)目中的文件類(lèi)型定義 extensions舶衬,加快解析速度埠通。(如:resolve: { extensions: ['.tsx', '.ts', '.js'] }
      7. 多進(jìn)程(如 babel-loader 構(gòu)建時(shí)間較長(zhǎng),使用 thread-loader 可將 loader 放在獨(dú)立的 work 池中運(yùn)行逛犹,僅對(duì)非常耗時(shí)的 loader 使用)
      8. 其他:區(qū)分環(huán)境([fullhash]/[chunkhash]/[contenthash])devtool 設(shè)置
  • 傳輸性能

    • 減小打包體積

      1. js 壓縮(webpack5 開(kāi)箱即用端辱,默認(rèn)開(kāi)啟多進(jìn)程與緩存:terser-webpack- plugin)
      2. css 壓縮( optimize-css-assets-webpack-plugin)
      3. splitChunks
        3.1 新的 chunk 可以被共享,或者模塊來(lái)自于 node_modules 文件夾
        3.2 新的 chunk 體積大于 20kb(在進(jìn)行 min+gz 之前的體積)
        3.3 當(dāng)按需加載 chunks 時(shí)虽画,并行請(qǐng)求的最大數(shù)量小于或等于 30
        當(dāng)加載初始化頁(yè)面時(shí)舞蔽,并發(fā)請(qǐng)求的最大數(shù)量小于或等于 30
      4. css 文件分離(mini-css-extract-plugin)
      5.      Tree Shaking(搖樹(shù))通過(guò)配置:sideEffects,只能清除無(wú)副作用的引用,有副作用需要通過(guò)優(yōu)化引用的方式码撰。(css Tree Shaking:purgecss-webpack-plugin)
        
      6. CDN 加速:將字體渗柿,圖片等靜態(tài)資源上傳 CDN
  • 運(yùn)行性能

    • 加快加載速度

      1. import 動(dòng)態(tài)導(dǎo)入
      2. 瀏覽器緩存,創(chuàng)建 hash id
      3. moduleIds: "deterministic", 公共包 hash 不因?yàn)樾碌囊蕾嚩淖?/li>
      4. 靜態(tài)資源使用 cdn 緩存
  • 總結(jié)

    小型項(xiàng)目脖岛,添加過(guò)多優(yōu)化配置朵栖,反而會(huì)因?yàn)樘砑宇~外的 loader 與 plugin 增加構(gòu)建時(shí)間
    構(gòu)建階段,使用 cache柴梆,可大大加快二次構(gòu)建速度
    減少打包體積陨溅,作用最大的是壓縮代碼,分離重復(fù)代碼绍在,Tree Shaking 作用也比較大
    加載速度:按需加載门扇,瀏覽器緩存,CDN 緩存效果都不錯(cuò)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末偿渡,一起剝皮案震驚了整個(gè)濱河市臼寄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌溜宽,老刑警劉巖吉拳,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異适揉,居然都是意外死亡留攒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)涡扼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人盟庞,你說(shuō)我怎么就攤上這事吃沪。” “怎么了什猖?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵票彪,是天一觀的道長(zhǎng)红淡。 經(jīng)常有香客問(wèn)我,道長(zhǎng)降铸,這世上最難降的妖魔是什么在旱? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮推掸,結(jié)果婚禮上桶蝎,老公的妹妹穿的比我還像新娘。我一直安慰自己谅畅,他們只是感情好登渣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著毡泻,像睡著了一般胜茧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仇味,一...
    開(kāi)封第一講書(shū)人閱讀 51,624評(píng)論 1 305
  • 那天呻顽,我揣著相機(jī)與錄音,去河邊找鬼丹墨。 笑死第练,一個(gè)胖子當(dāng)著我的面吹牛襟己,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼旗芬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了秋忙?” 一聲冷哼從身側(cè)響起秸弛,我...
    開(kāi)封第一講書(shū)人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搪搏,沒(méi)想到半個(gè)月后狭握,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疯溺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年论颅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片囱嫩。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡恃疯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出墨闲,到底是詐尸還是另有隱情今妄,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站盾鳞,受9級(jí)特大地震影響犬性,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腾仅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一乒裆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧推励,春花似錦鹤耍、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至受神,卻和暖如春抛猖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鼻听。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工财著, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人撑碴。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓撑教,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親醉拓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伟姐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容