Webpack 原理淺析

来源:https://www.cnblogs.com/o2team/archive/2020/07/29/13398432.html
-Advertisement-
Play Games

作者: 凹凸曼 - 風魔小次郎 背景 Webpack 迭代到4.x版本後,其源碼已經十分龐大,對各種開發場景進行了高度抽象,閱讀成本也愈發昂貴。但是為了瞭解其內部的工作原理,讓我們嘗試從一個最簡單的 webpack 配置入手,從工具設計者的角度開發一款低配版的 Webpack。 開發者視角 假設某一 ...


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

背景

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="./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),不定時推送文章:

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在html中如果要把多餘的文字顯示為省略號,那麼有以下幾種方法: 單行文本: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-widt ...
  • 在很多網站上我們都看到input輸入框顯示提示文字,讓我們一起來看看如果在input輸入框中顯示提示文字。我們只需要在<input>標簽里添加:placeholder="提示文字即可",那麼如果要修改提示文字的樣式呢?可以這樣設置css樣式: <!DOCTYPE html> <html> <head ...
  • 在做表單提交時我們有時會遇到這樣的需求,在用戶沒有輸入必填信息點提交時提示文字需要改為我們想要的提示信息,那麼可以在input裡面增加這樣的語句: <input type="text" placeholder="您的姓名" required oninvalid="setCustomValidity( ...
  • 最近在做一個頁面時,發現在 iPad 的 Safari 瀏覽器中背景顯示不全,定位到該 div 後發現所指定 css 的寬度為 100% ; 到百度搜索後發現,safari 中 viewport 預設寬度為 980px,若事先未指定其初始 viewport 寬度,則會預設按照 980px 處理。 可 ...
  • 我們在使用html編寫一個網站的時候,通常情況下頭部和尾部是相同的,如果一個網站的每個頁面都把這些代碼寫一遍,不僅浪費時間,還顯得重覆代碼很多,所以此時把重覆的頁面單獨摘出來,在用到的時候從外部直接引進去,就能節省很多時間,減少很多代碼。 在這裡,有好幾種引入html文件的方式,不過每種都是有利有弊 ...
  • 一:完整代碼 ;(function (w){ function createElement(type, attribute, ...childs){ //創建虛擬DOM let element = { type: '', attribute: {}, childs: [] }; element.ty ...
  • “智慧園區管控系統”基於物聯網生態體系操控平臺架構,利用新一代信息與通信技術來感知、監測、分析、控制、整合園區各個關鍵環節的資源,集成了光伏、變電站、停車場等管控場景界面,使各系統之間互聯、共用、智慧,實現了多信息協同聯動,為園區安全管理、業主便捷生活、物業信息化管理提供了有力保障。有效的降低了企業... ...
  • 俗話說:“物以類聚,人以群分”“近朱者赤,近墨者黑”;一個良好的文件夾存放規範,往往能給WEB開發團隊帶來整體的提高渲染效率、方便維護代碼等必不可少的效果。 | |文件夾命名 |存放說明 | | | | | |_root/根目錄 | | | | |cn |中文html文件 | | |css |層疊樣 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...