主頁 > 企業開發 > Webpack 原理淺析

Webpack 原理淺析

2020-09-13 14:18:30 企業開發

作者: 凹凸曼 - 風魔小次郎

背景

Webpack 迭代到4.x版本后,其原始碼已經十分龐大,對各種開發場景進行了高度抽象,閱讀成本也愈發昂貴,但是為了了解其內部的作業原理,讓我們嘗試從一個最簡單的 webpack 配置入手,從工具設計者的角度開發一款低配版的 Webpack

開發者視角

假設某一天,我們接到了需求,需要開發一個 react 單頁面應用,頁面中包含一行文字和一個按鈕,需要支持每次點擊按鈕的時候讓文字發生變化,于是我們新建了一個專案,并且在 [根目錄]/src 下新建 JS 檔案,為了模擬 Webpack 追蹤模塊依賴進行打包的程序,我們新建了 3 個 React 組件,并且在他們之間建立起一個簡單的依賴關系,

// index.js 根組件
import React from 'react'
import ReactDom from 'react-dom'
import App from './App'
ReactDom.render(<App />, document.querySelector('#container'))
// App.js 頁面組件
import React from 'react'
import Switch from './Switch.js'
export default class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      toggle: false
    }
  }
  handleToggle() {
    this.setState(prev => ({
      toggle: !prev.toggle
    }))
  }
  render() {
    const { toggle } = this.state
    return (
      <div>
        <h1>Hello, { toggle ? 'NervJS' : 'O2 Team'}</h1>
        <Switch handleToggle={this.handleToggle.bind(this)} />
      </div>
    )
  }
}
// Switch.js 按鈕組件
import React from 'react'

export default function Switch({ handleToggle }) {
  return (
    <button onClick={handleToggle}>Toggle</button>
  )
}

接著我們需要一個組態檔讓 Webpack 知道我們期望它如何作業,于是我們在根目錄下新建一個檔案 webpack.config.js 并且向其中寫入一些基礎的配置,(如果不太熟悉配置內容可以先學習webpack中文檔案)

// webpack.config.js
const resolve = dir => require('path').join(__dirname, dir)

module.exports = {
  // 入口檔案地址
  entry: './src/index.js',
  // 輸出檔案地址
  output: {
		path: resolve('dist'),
    fileName: 'bundle.js'
  },
  // loader
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        // 編譯匹配include路徑的檔案
        include: [
          resolve('src')
        ],
        use: 'babel-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

其中 module 的作用是在 test 欄位和檔案名匹配成功時就用對應的 loader 對代碼進行編譯,Webpack本身只認識 .js.json 這兩種型別的檔案,而通過loader,我們就可以對例如 css 等其他格式的檔案進行處理,

而對于 React 檔案而言,我們需要將 JSX 語法轉換成純 JS 語法,即 React.createElement 方法,代碼才可能被瀏覽器所識別,平常我們是通過 babel-loader 并且配置好 react 的決議規則來做這一步,

經過以上處理之后,瀏覽器真正閱讀到的按鈕組件代碼其實大概是這個樣子的,

...
function Switch(_ref) {
  var handleToggle = _ref.handleToggle;
  return _nervjs["default"].createElement("button", {
    onClick: handleToggle
  }, "Toggle");
}

而至于 plugin 則是一些插件,這些插件可以將對編譯結果的處理函式注冊在 Webpack 的生命周期鉤子上,在生成最終檔案之前對編譯的結果做一些處理,比如大多數場景下我們需要將生成的 JS 檔案插入到 Html 檔案中去,就需要使用到 html-webpack-plugin 這個插件,我們需要在配置中這樣寫,

const HtmlWebpackPlugin = require('html-webpack-plugin');

const webpackConfig = {
  entry: 'index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'index_bundle.js'
  },
  // 向plugins陣列中傳入一個HtmlWebpackPlugin插件的實體
  plugins: [new HtmlWebpackPlugin()]
};

這樣,html-webpack-plugin 會被注冊在打包的完成階段,并且會獲取到最終打包完成的入口 JS 檔案路徑,生成一個形如 <script src=https://www.cnblogs.com/o2team/p/"./dist/bundle_[hash].js"></script> 的 script 標簽插入到 Html 中,這樣瀏覽器就可以通過 html 檔案來展示頁面內容了,

ok,寫到這里,對于一個開發者而言,所有配置項和需要被打包的工程代碼檔案都已經準備完畢,接下來需要的就是將作業交給打包工具 Webpack,通過 Webpack 將代碼打包成我們和瀏覽器希望看到的樣子

工具視角

首先,我們需要了解Webpack打包的流程

Webpack 的作業流程中可以看出,我們需要實作一個 Compiler 類,這個類需要收集開發者傳入的所有配置資訊,然后指揮整體的編譯流程,我們可以把 Compiler 理解為公司老板,它統領全域,并且掌握了全域資訊(客戶需求),在了解了所有資訊后它會呼叫另一個類 Compilation 生成實體,并且將所有的資訊和作業流程托付給它,Compilation 其實就相當于老板的秘書,需要去調動各個部門按照要求開始作業,而 loaderplugin 則相當于各個部門,只有在他們專長的作業( js , css , scss , jpg , png...)出現時才會去處理

為了既實作 Webpack 打包的功能,又只實作核心代碼,我們對這個流程做一些簡化

首先我們新建了一個 webpack 函式作為對外暴露的方法,它接受兩個引數,其中一個是配置項物件,另一個則是錯誤回呼,

const Compiler = require('./compiler')

function webpack(config, callback) {
  // 此處應有引數校驗
  const compiler = new Compiler(config)
  // 開始編譯
  compiler.run()
}

module.exports = webpack

1. 構建配置資訊

我們需要先在 Compiler 類的構造方法里面收集用戶傳入的資訊

class Compiler {
  constructor(config, _callback) {
    const {
      entry,
      output,
      module,
      plugins
    } = config
    // 入口
    this.entryPath = entry
    // 輸出檔案路徑
    this.distPath = output.path
    // 輸出檔案名稱
    this.distName = output.fileName
    // 需要使用的loader
    this.loaders = module.rules
    // 需要掛載的plugin
    this.plugins = plugins
     // 根目錄
    this.root = process.cwd()
     // 編譯工具類Compilation
    this.compilation = {}
    // 入口檔案在module中的相對路徑,也是這個模塊的id
    this.entryId = getRootPath(this.root, entry, this.root)
  }
}

同時,我們在建構式中將所有的 plugin 掛載到實體的 hooks 屬性中去,Webpack 的生命周期管理基于一個叫做 tapable 的庫,通過這個庫,我們可以非常方便的創建一個發布訂閱模型的鉤子,然后通過將函式掛載到實體上(鉤子事件的回呼支持同步觸發、異步觸發甚至進行鏈式回呼),在合適的時機觸發對應事件的處理函式,我們在 hooks 上宣告一些生命周期鉤子:

const { AsyncSeriesHook } = require('tapable') // 此處我們創建了一些異步鉤子
constructor(config, _callback) {
  ...
  this.hooks = {
    // 生命周期事件
    beforeRun: new AsyncSeriesHook(['compiler']), // compiler代表我們將向回呼事件中傳入一個compiler引數
    afterRun: new AsyncSeriesHook(['compiler']),
    beforeCompile: new AsyncSeriesHook(['compiler']),
    afterCompile: new AsyncSeriesHook(['compiler']),
    emit: new AsyncSeriesHook(['compiler']),
    failed: new AsyncSeriesHook(['compiler']),
  }
  this.mountPlugin()
}
// 注冊所有的plugin
mountPlugin() {
  for(let i=0;i<this.plugins.length;i++) {
    const item = this.plugins[i]
    if ('apply' in item && typeof item.apply === 'function') {
      // 注冊各生命周期鉤子的發布訂閱監聽事件
      item.apply(this)
    }
  }
}
// 當運行run方法的邏輯之前
run() {
  // 在特定的生命周期發布訊息,觸發對應的訂閱事件
  this.hooks.beforeRun.callAsync(this) // this作為引數傳入,對應之前的compiler
  ...
}

冷知識:
每一個 plugin Class 都必須實作一個 apply 方法,這個方法接收 compiler 實體,然后將真正的鉤子函式掛載到 compiler.hook 的某一個宣告周期上,
如果我們宣告了一個hook但是沒有掛載任何方法,在 call 函式觸發的時候是會報錯的,但是實際上 Webpack 的每一個生命周期鉤子除了掛載用戶配置的 plugin ,都會掛載至少一個 Webpack 自己的 plugin,所以不會有這樣的問題,更多關于 tapable 的用法也可以移步 Tapable

2. 編譯

接下來我們需要宣告一個 Compilation 類,這個類主要是執行編譯作業,在 Compilation 的建構式中,我們先接收來自老板 Compiler 下發的資訊并且掛載在自身屬性中,

class Compilation {
  constructor(props) {
    const {
      entry,
      root,
      loaders,
      hooks
    } = props
    this.entry = entry
    this.root = root
    this.loaders = loaders
    this.hooks = hooks
  }
  // 開始編譯
  async make() {
    await this.moduleWalker(this.entry)
  }
  // dfs遍歷函式
  moduleWalker = async () => {}
}

因為我們需要將打包程序中參考過的檔案都編譯到最終的代碼包里,所以需要宣告一個深度遍歷函式 moduleWalker (這個名字是筆者取的,不是webpack官方取的),顧名思義,這個方法將會從入口檔案開始,依次對檔案進行第一步和第二步編譯,并且收集參考到的其他模塊,遞回進行同樣的處理,

編譯步驟分為兩步

  1. 第一步是使用所有滿足條件的 loader 對其進行編譯并且回傳編譯之后的源代碼
  2. 第二步相當于是 Webpack 自己的編譯步驟,目的是構建各個獨立模塊之間的依賴呼叫關系,我們需要做的是將所有的 require 方法替換成 Webpack 自己定義的 __webpack_require__ 函式,因為所有被編譯后的模塊將被 Webpack 存盤在一個閉包的物件 moduleMap 中,而 __webpack_require__ 函式則是唯一一個有權限訪問 moduleMap 的方法,

一句話解釋 __webpack_require__的作用就是,將模塊之間原本 檔案地址 -> 檔案內容 的關系替換成了 物件的key -> 物件的value(檔案內容) 這樣的關系,

在完成第二步編譯的同時,會對當前模塊內的參考進行收集,并且回傳到 Compilation 中, 這樣moduleWalker 才能對這些依賴模塊進行遞回的編譯,當然其中大概率存在回圈參考和重復參考,我們會根據參考檔案的路徑生成一個獨一無二的 key 值,在 key 值重復時進行跳過,

i. moduleWalker 遍歷函式

// 存放處理完畢的模塊代碼Map
moduleMap = {}

// 根據依賴將所有被參考過的檔案都進行編譯
async moduleWalker(sourcePath) {
  if (sourcePath in this.moduleMap) return
  // 在讀取檔案時,我們需要完整的以.js結尾的檔案路徑
  sourcePath = completeFilePath(sourcePath)
  const [ sourceCode, md5Hash ] = await this.loaderParse(sourcePath)
  const modulePath = getRootPath(this.root, sourcePath, this.root)
  // 獲取模塊編譯后的代碼和模塊內的依賴陣列
  const [ moduleCode, relyInModule ] = this.parse(sourceCode, path.dirname(modulePath))
  // 將模塊代碼放入ModuleMap
  this.moduleMap[modulePath] = moduleCode
  this.assets[modulePath] = md5Hash
  // 再依次對模塊中的依賴項進行決議
  for(let i=0;i<relyInModule.length;i++) {
    await this.moduleWalker(relyInModule[i], path.dirname(relyInModule[i]))
  }
}

如果將dfs的路徑給log出來,我們就可以看到這樣的流程

ii. 第一步編譯 loaderParse函式

async loaderParse(entryPath) {
  // 用utf8格式讀取檔案內容
  let [ content, md5Hash ] = await readFileWithHash(entryPath)
  // 獲取用戶注入的loader
  const { loaders } = this
  // 依次遍歷所有loader
  for(let i=0;i<loaders.length;i++) {
    const loader = loaders[i]
    const { test : reg, use } = loader
    if (entryPath.match(reg)) {
      // 判斷是否滿足正則或字串要求
      // 如果該規則需要應用多個loader,從最后一個開始向前執行
      if (Array.isArray(use)) {
        while(use.length) {
          const cur = use.pop()
          const loaderHandler = 
            typeof cur.loader === 'string' 
            // loader也可能來源于package包例如babel-loader
              ? require(cur.loader)
              : (
                typeof cur.loader === 'function'
                ? cur.loader : _ => _
              )
          content = loaderHandler(content)
        }
      } else if (typeof use.loader === 'string') {
        const loaderHandler = require(use.loader)
        content = loaderHandler(content)
      } else if (typeof use.loader === 'function') {
        const loaderHandler = use.loader
        content = loaderHandler(content)
      }
    }
  }
  return [ content, md5Hash ]
}

然而這里遇到了一個小插曲,就是我們平常使用的 babel-loader 似乎并不能在 Webpack 包以外的場景被使用,在 babel-loader 的檔案中看到了這樣一句話

This package allows transpiling JavaScript files using Babel and webpack.

不過好在 @babel/corewebpack 并無聯系,所以只能辛苦一下,再手寫一個 loader 方法去決議 JSES6 的語法,

const babel = require('@babel/core')

module.exports = function BabelLoader (source) {
  const res = babel.transform(source, {
    sourceType: 'module' // 編譯ES6 import和export語法
  })
  return res.code
}

當然,編譯規則可以作為配置項傳入,但是為了模擬真實的開發場景,我們需要配置一下 babel.config.js檔案

module.exports = function (api) {
  api.cache(true)
  return {
    "presets": [
      ['@babel/preset-env', {
        targets: {
          "ie": "8"
        },
      }],
      '@babel/preset-react', // 編譯JSX
    ],
    "plugins": [
      ["@babel/plugin-transform-template-literals", {
        "loose": true
      }]
    ],
    "compact": true
  }
}

于是,在獲得了 loader 處理過的代碼之后,理論上任何一個模塊都已經可以在瀏覽器或者單元測驗中直接使用了,但是我們的代碼是一個整體,還需要一種合理的方式來組織代碼之間互相參考的關系,

上面也解釋了我們為什么要使用 __webpack_require__ 函式,這里我們得到的代碼仍然是字串的形式,為了方便我們使用 eval 函式將字串決議成直接可讀的代碼,當然這只是求快的方式,對于 JS 這種解釋型語言,如果一個一個模塊去解釋編譯的話,速度會非常慢,事實上真正的生產環境會將模塊內容封裝成一個 IIFE(立即自執行函式運算式)

總而言之,在第二部編譯 parse 函式中我們需要做的事情其實很簡單,就是將所有模塊中的 require 方法的函式名稱替換成 __webpack_require__ 即可,我們在這一步使用的是 babel 全家桶, babel 作為業內頂尖的JS編譯器,分析代碼的步驟主要分為兩步,分別是詞法分析和語法分析,簡單來說,就是對代碼片段進行逐詞分析,根據當前單詞生成一個背景關系語境,然后進行再判斷下一個單詞在背景關系語境中所起的作用,

注意,在這一步中我們還可以“順便”搜集模塊的依賴項陣列一同回傳(用于 dfs 遞回)

const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const types = require('@babel/types')
const generator = require('@babel/generator').default
...
// 決議原始碼,替換其中的require方法來構建ModuleMap
parse(source, dirpath) {
  const inst = this
  // 將代碼決議成ast
  const ast = parser.parse(source)
  const relyInModule = [] // 獲取檔案依賴的所有模塊
  traverse(ast, {
    // 檢索所有的詞法分析節點,當遇到函式呼叫運算式的時候執行,對ast樹進行改寫
    CallExpression(p) {
      // 有些require是被_interopRequireDefault包裹的
      // 所以需要先找到_interopRequireDefault節點
      if (p.node.callee && p.node.callee.name === '_interopRequireDefault') {
        const innerNode = p.node.arguments[0]
        if (innerNode.callee.name === 'require') {
          inst.convertNode(innerNode, dirpath, relyInModule)
        }
      } else if (p.node.callee.name === 'require') {
        inst.convertNode(p.node, dirpath, relyInModule)
      }
    }
  })
  // 將改寫后的ast樹重新組裝成一份新的代碼, 并且和依賴項一同回傳
  const moduleCode = generator(ast).code
  return [ moduleCode, relyInModule ]
}
/**
 * 將某個節點的name和arguments轉換成我們想要的新節點
 */
convertNode = (node, dirpath, relyInModule) => {
  node.callee.name = '__webpack_require__'
  // 引數字串名稱,例如'react', './MyName.js'
  let moduleName = node.arguments[0].value
  // 生成依賴模塊相對【專案根目錄】的路徑
  let moduleKey = completeFilePath(getRootPath(dirpath, moduleName, this.root))
  // 收集module陣列
  relyInModule.push(moduleKey)
  // 替換__webpack_require__的引數字串,因為這個字串也是對應模塊的moduleKey,需要保持統一
  // 因為ast樹中的每一個元素都是babel節點,所以需要使用'@babel/types'來進行生成
  node.arguments = [ types.stringLiteral(moduleKey) ]
}

3. emit 生成bundle檔案

執行到這一步, compilation 的使命其實就已經完成了,如果我們平時有去觀察生成的 js 檔案的話,會發現打包出來的樣子是一個立即執行函式,主函式體是一個閉包,閉包中快取了已經加載的模塊 installedModules ,以及定義了一個 __webpack_require__ 函式,最侄訓傳的是函式入口所對應的模塊,而函式的引數則是各個模塊的 key-value 所組成的物件,

我們在這里通過 ejs 模板去進行拼接,將之前收集到的 moduleMap 物件進行遍歷,注入到ejs模板字串中去,

模板代碼

// template.ejs
(function(modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
      // Check if module is in cache
      if(installedModules[moduleId]) {
          return installedModules[moduleId].exports;
      }
      // Create a new module (and put it into the cache)
      var module = installedModules[moduleId] = {
          i: moduleId,
          l: false,
          exports: {}
      };
      // Execute the module function
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
      // Flag the module as loaded
      module.l = true;
      // Return the exports of the module
      return module.exports;
  }
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})({
 <%for(let key in modules) {%>
     "<%-key%>":
         (function(module, exports, __webpack_require__) {
             eval(
                 `<%-modules[key]%>`
             );
         }),
     <%}%>
});

生成bundle.js

/**
 * 發射檔案,生成最終的bundle.js
 */
emitFile() { // 發射打包后的輸出結果檔案
  // 首先對比快取判斷檔案是否變化
  const assets = this.compilation.assets
  const pastAssets = this.getStorageCache()
  if (loadsh.isEqual(assets, pastAssets)) {
    // 如果檔案hash值沒有變化,說明無需重寫檔案
    // 只需要依次判斷每個對應的檔案是否存在即可
    // 這一步省略!
  } else {
    // 快取未能命中
    // 獲取輸出檔案路徑
    const outputFile = path.join(this.distPath, this.distName);
    // 獲取輸出檔案模板
    // const templateStr = this.generateSourceCode(path.join(__dirname, '..', "bundleTemplate.ejs"));
    const templateStr = fs.readFileSync(path.join(__dirname, '..', "template.ejs"), 'utf-8');
    // 渲染輸出檔案模板
    const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.compilation.moduleMap});
    
    this.assets = {};
    this.assets[outputFile] = code;
    // 將渲染后的代碼寫入輸出檔案中
    fs.writeFile(outputFile, this.assets[outputFile], function(e) {
      if (e) {
        console.log('[Error] ' + e)
      } else {
        console.log('[Success] 編譯成功')
      }
    });
    // 將快取資訊寫入快取檔案
    fs.writeFileSync(resolve(this.distPath, 'manifest.json'), JSON.stringify(assets, null, 2))
  }
}

在這一步中我們根據檔案內容生成的 Md5Hash 去對比之前的快取來加快打包速度,細心的同學會發現 Webpack 每次打包都會生成一個快取檔案 manifest.json,形如

{
  "main.js": "./js/main7b6b4.js",
  "main.css": "./css/maincc69a7ca7d74e1933b9d.css",
  "main.js.map": "./js/main7b6b4.js.map",
  "vendors~main.js": "./js/vendors~main3089a.js",
  "vendors~main.css": "./css/vendors~maincc69a7ca7d74e1933b9d.css",
  "vendors~main.js.map": "./js/vendors~main3089a.js.map",
  "js/28505f.js": "./js/28505f.js",
  "js/28505f.js.map": "./js/28505f.js.map",
  "js/34c834.js": "./js/34c834.js",
  "js/34c834.js.map": "./js/34c834.js.map",
  "js/4d218c.js": "./js/4d218c.js",
  "js/4d218c.js.map": "./js/4d218c.js.map",
  "index.html": "./index.html",
  "static/initGlobalSize.js": "./static/initGlobalSize.js"
}

這也是檔案斷點續傳中常用到的一個判斷,這里就不做詳細的展開了


檢驗

做完這一步,我們已經基本大功告成了(誤:如果不考慮令人智息的debug程序的話),接下來我們在 package.json 里面配置好打包腳本

"scripts": {
  "build": "node build.js"
}

運行 yarn build

(@ο@) 哇~激動人心的時刻到了,

然而...

看著打包出來的這一坨奇怪的東西報錯,心里還是有點想笑的,檢查了一下發現是因為反引號遇到注釋中的反引號于是拼接字串提前結束了,好吧,那么我在 babel traverse 時加了幾句代碼,洗掉掉了代碼中所有的注釋,但是隨之而來的又是一些其他的問題,

好吧,可能在實際 react 生產打包中還有一些其他的步驟,但是這不在今天討論的話題當中,此時,鬼魅的框架涌上心頭,我腦中想起了京東凹凸實驗室自研的高性能,兼容性優秀,緊跟 react 版本的類react框架 NervJS ,或許 NervJS 平易近人(誤)的代碼能夠支持這款令人抱歉的打包工具

于是我們在 babel.config.js 中配置alias來替換 react 依賴項,(React專案轉NervJS就是這么簡單)

module.exports = function (api) {
  api.cache(true)
  return {
		...
    "plugins": [
			...
      [
        "module-resolver", {
          "root": ["."],
          "alias": {
            "react": "nervjs",
            "react-dom": "nervjs",
            // Not necessary unless you consume a module using `createClass`
            "create-react-class": "nerv-create-class"
          }
        }
      ]
    ],
    "compact": true
  }
}

運行 yarn build


(@ο@) 哇~代碼終于成功運行了起來,雖然存在著許多的問題,但是至少這個 webpack 在設計如此簡單的情況下已經有能力支持大部分JS框架了,感興趣的同學也可以自己嘗試寫一寫,或者直接從這里clone下來看

毫無疑問,Webpack 是一個非常優秀的代碼模塊打包工具(雖然它的官網非常低調的沒有任何slogen),一款非常優秀的工具,必然是在保持了自己本身的特性的同時,同時能夠賦予其他開發者在其基礎上拓展設想之外作品的能力,如果有能力深入學習這些工具,對于我們在代碼工程領域的認知也會有很大的提升,

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

歡迎關注凹凸實驗室公眾號

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/25033.html

標籤:JavaScript

上一篇:keepalived行程數量例外問題

下一篇:實作虛擬DOM

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • 使用Django Rest framework搭建Blog

    在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處于上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ......

    uj5u.com 2023-04-20 08:17:54 more
  • 記錄-new Date() 我忍你很久了!

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下面,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ......

    uj5u.com 2023-04-20 08:17:47 more
  • 使用Vue.js實作文字跑馬燈效果

    實作文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實作代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ......

    uj5u.com 2023-04-20 08:12:31 more
  • JavaScript 運算子

    JavaScript 運算子/運算子 在 JavaScript 中,有一些運算子可以使代碼更簡潔、易讀和高效。以下是一些常見的運算子: 1、可選鏈運算子(optional chaining operator) ?.是可選鏈運算子(optional chaining operator)。?. 可選鏈操 ......

    uj5u.com 2023-04-20 08:02:25 more
  • CSS—相對單位rem

    一、概述 rem是一個相對長度單位,它的單位長度取決于根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般默認為16px,即默認情況下: 1rem = 16px rem布局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實作rem單位隨螢屏尺寸的變化,如 ......

    uj5u.com 2023-04-20 08:02:21 more
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 08:01:50 more
  • 如何在 vue3 中使用 jsx/tsx?

    我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單檔案組件模式,即一個組件就是一個檔案,但其實 Vue 也是支持使用 JSX 來撰寫組件的。這里不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速了解和使用 Vu ......

    uj5u.com 2023-04-20 08:01:37 more
  • 【Vue2.x原始碼系列06】計算屬性computed原理

    本章目標:計算屬性是如何實作的?計算屬性快取原理以及洋蔥模型的應用?在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ......

    uj5u.com 2023-04-20 08:01:31 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:01:10 more
  • http1.1與http2.0

    一、http是什么 通俗來講,http就是計算機通過網路進行通信的規則,是一個基于請求與回應,無狀態的,應用層協議。常用于TCP/IP協議傳輸資料。目前任何終端之間任何一種通信方式都必須按Http協議進行,否則無法連接。tcp(三次握手,四次揮手)。 請求與回應:客戶端請求、服務端回應資料。 無狀態 ......

    uj5u.com 2023-04-20 08:00:32 more