如何將markdown轉換為wxml

来源:https://www.cnblogs.com/edwardloveyou/archive/2019/04/27/10775834.html
-Advertisement-
Play Games

話說我要為技術博客寫一個小程式版,我的博客解決方案是 hexo + github page ,格式當然是技術控們喜歡的 markdown 了 。但小程式使用的卻是獨有的模版語言 WXML 。我總不能把之前的文章手動轉換成小程式的 wxml 格式吧,而網上也沒完善的轉換庫,還是自己寫個解析器吧。 解析 ...


話說我要為技術博客寫一個小程式版,我的博客解決方案是 hexo + github-page,格式當然是技術控們喜歡的 markdown 了 。但小程式使用的卻是獨有的模版語言 WXML。我總不能把之前的文章手動轉換成小程式的 wxml 格式吧,而網上也沒完善的轉換庫,還是自己寫個解析器吧。

解析器最核心的部分就是字元串模式匹配,既然涉及到字元串匹配,那麼就離不開正則表達式。幸好,正則表達式是我的優勢之一。

正則表達式

JavaScript中的正則表達式

解析器涉及到的 JavaScript 正則表達式知識

RegExp 構造函數屬性,其中lastMatch,rightContent在字元串截取時非常有用

長屬性名 短屬性名 替換標誌 說明
input $_ 最近一次要匹配的字元串。Opera未實現此屬性
lastMatch $& $& 最近一次的匹配項。Opera未實現此屬性
lastParen $+ 最近一次匹配的捕獲組。Opera未實現此屬性
leftContext $` $` input字元串中lastMatch之前的文本
rightContext $' $' Input字元串中lastMatch之後的文本
multiline $* 布爾值,表示是否所有表達式都使用多行模式。IE和Opera未實現此屬性
$n $n 分組
$$ 轉義$
  1. test 方法 和 RegExp 構造函數
    test 方法調用後,上面的屬性就會出現在 RegExp 中,不推薦使用短屬性名,因為會造成代碼可讀性的問題,下麵就是樣例

    var text = "this has been a short summer";
    var pattern = /(.)hort/g;
    
    if (pattern.test(text)){
        alert(RegExp.input);         // this has been a short summer
        alert(RegExp.leftContext);   // this has been a
        alert(RegExp.rightContext);  // summer
        alert(RegExp.lastMatch);     // short
        alert(RegExp.lastParen);     // s
        alert(RegExp.multiline);     // false
    }
    
    //長屬性名都可以用相應的短屬性名來代替。不過由於這些短屬性名大都不是有效的ECMAScript標識符,因此必須通過方括弧語法來訪問它們
    if (pattern.test(text)){
        alert(RegExp.$_);
        alert(RegExp["$`"]);
        alert(RegExp["$'"]);
        alert(RegExp["$&"]);
        alert(RegExp["$+"]);
        alert(RegExp["$*"]);
    }
  2. replace 方法

    一般使用的是沒有回調函數的簡單版本,而回調函數版本則是個大殺器,及其強大

    //簡單替換, replace預設只進行一次替換, 如設定全局模式,  將會對符合條件的子字元串進行多次替換,最後返回經過多次替換的結果字元串.
    var regex = /(\d{4})-(\d{2})-(\d{2})/;
    "2011-11-11".replace(regex, "$2/$3/$1");
    
    //replace 使用回調函數自定義替換,必須啟用全局模式g,因為要不斷向前匹配,直到匹配完整個字元串
    //match為當前匹配到的字元串,index為當前匹配結果在字元串中的位置,sourceStr表示原字元串,
    //如果有分組,則中間多了匹配到的分組內容,match,group1(分組1)...groupN(分組n),index,sourceStr
    "one two three".replace(/\bt[a-zA-Z]+\b/g, function (match,index,str) { //將非開頭的單詞大寫
        console.log(match,index,str);
        return match.toUpperCase(); 
    });
  3. match 方法

    全局模式和非全局模式有顯著的區別,全局模式和 exec 方法類似。

    // 如果參數中傳入的是子字元串或是沒有進行全局匹配的正則表達式,那麼match()方法會從開始位置執行一次匹配,如果沒有匹配到結果,則返回null.否則則會返回一個數組,該數組的第0個元素存放的是匹配文本,返回的數組還含有兩個對象屬性index和input,分別表示匹配文本的起始字元索引和原字元串,還有分組屬性
    var str = '1a2b3c4d5e';
    console.log(str.match(/b/)); //返回["b", index: 3, input: "1a2b3c4d5e"]
    
    //如果參數傳入的是具有全局匹配的正則表達式,那麼match()從開始位置進行多次匹配,直到最後.如果沒有匹配到結果,則返回null.否則則會返回一個數組,數組中存放所有符合要求的子字元串,但沒有index和input屬性,也沒有分組屬性
    var str = '1a2b3c4d5e';
    str.match(/h/g); //返回null
    str.match(/\d/g); //返回["1", "2", "3", "4", "5"]
    
    var pattern = /\d{4}-\d{2}-\d{2}/g;
    var str ="2010-11-10 2012-12-12";
    var matchArray = str.match(pattern);
    for(vari = 0; i < matchArray.length; i++) {
         console.log(matchArray[i]);
    }
  4. exec 方法

    與全局模式下的 match 類似,但 exec 更強大,因為返回結果包含各種匹配信息,而match全局模式是不包含具體匹配信息的。

    //逐步提取,捕獲分組匹配文本,必須使用全局模式g, 成功則返回數組(包含匹配的分組信息), 否則為null
    //Regex每次匹配成功後,會把匹配結束位置更新到lastIndex,下次從lastIndex開始匹配
    //如果不指定全局模式,使用while迴圈,會造成無窮迴圈
    var pattern = /(\d{4})-(\d{2})-(\d{2})/g;
    var str2 = "2011-11-11 2013-13-13" ;
    while ((matchArray = pattern.exec(str2)) != null) {
      console.log( "date: " + matchArray[0]+"start at:" + matchArray.index+" ends at:"+         pattern.lastIndex);
      console.log( ",year: " + matchArray[1]);
      console.log( ",month: " + matchArray[2]);
      console.log( ",day: " + matchArray[3]);
    }
  5. searchsplit 這兩個比較簡單的方法則不再介紹

正則表達式高級概念

正常情況下正則是從左向右進行單字元匹配,每匹配到一個字元, 就後移位置, 直到最終消耗完整個字元串, 這就是正則表達式的字元串匹配過程,也就是它會匹配字元,占用字元。相關的基本概念不再講解,這裡要講的和字元匹配不同的概念 - 斷言。

  1. 斷言

    正則中大多數結構都是匹配字元,而斷言則不同,它不匹配字元,不占用字元,而只在某個位置判斷左/右側的文本是否符合要求。這類匹配位置的元素,可以稱為 "錨點",主要分為三類:單詞邊界,開始結束位置,環視。

    單詞邊界 \b 是這樣的位置,一邊是單詞字元,一邊不是單詞字元,如下字元串樣例所示

    \brow\b   //row
    \brow     //row, rowdy
    row\b     //row, tomorow

    ^ 行開頭,多行模式下亦匹配每個換行符後的位置,即行首
    $ 行結束,多行模式下亦匹配每個換行符前的位置,即行尾

    //js 中的 $ 只能匹配字元串的結束位置,不會匹配末尾換行符之前的換行符。但開啟多行模式(m)後,^ 和 $ 則可以匹配中間的換行符。 如下例子可驗證:
    
    // 預設全局模式下,^ 和 $ 直接匹配到了文本最開頭和末尾,忽略了中間的換行符
    'hello\nword'.replace(/^|$/g,'<p>')
    "<p>hello
    word<p>"
    
    // 多行模式下,同時能匹配到結束符中間的換行符
    'hello\nword\nhi'.replace(/^|$/mg,'<p>')
    "<p>hello<p>
    <p>word<p>
    <p>hi<p>"
  2. 環視

    環視是斷言中最強的存在,同樣不占用字元也不提取任何字元,只匹配文本中的特定位置,與\b, ^ $ 邊界符號相似;但環視更加強大,因為它可以指定位置和在指定位置處添加向前或向後驗證的條件。

    而環視主要體現在它的不占位(不消耗匹配字元), 因此又被稱為零寬斷言。所謂不占寬度,可以這樣理解:

    • 環視的匹配結果不納入數據結果;

    • 環視它匹配過的地方,下次還能用它繼續匹配。

    環視包括順序環視和逆序環視,javascript 在 ES 2018 才開始支持逆序環視

    • (?=) 順序肯定環視 匹配右邊
    • (?!) 順序否定環視
    • (?<=) 逆序肯定環視 匹配左邊
    • (?<!) 逆序否定環視

    來看一下具體的樣例

    // 獲取.exe尾碼的文件名,不使用分組捕獲,能使捕獲結果不包含.exe尾碼,充分利用了環視匹配結果同時不占位的特性
    'asd.exe'.match(/.+(?=\.exe)/)
    => ["asd", index: 0, input: "asd.exe", groups: undefined]
    
    // 變種否定順序環視,排除特定標簽p/a/img,匹配html標簽
    </?(?!p|a|img)([^> /]+)[^>]*/?> 
    
    //常規逆序環視,同樣利用了環視匹配不占位的特性
    /(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100",index: 29,...]
    /(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90", index: 21,...] 
    
    // 利用環視占位但不匹配的特性
    '12345678'.replace(/\B(?=(\d{3})+$)/g , ',') 
    => "12,345,678" //分割數字

解析器的編寫

正則表達式相關寫得有點多,但磨刀不誤砍柴工,開始進入主題

markdown格式

hexo 生成的 markdwon 文件格式如下,解析器就是要把它解析成json格式的輸出結果,供小程式輸出 wxml

---
title: Haskell學習-functor
date: 2018-08-15 21:27:15
tags: [haskell]
categories: 技術
banner: https://upload-images.jianshu.io/upload_images/127924-be9013350ffc4b88.jpg
---
<!-- 原文地址:[Haskell學習-functor](https://edwardzhong.github.io/2018/08/15/haskellc/) -->
## 什麼是Functor
**functor** 就是可以執行map操作的對象,functor就像是附加了語義的表達式,可以用盒子進行比喻。**functor** 的定義可以這樣理解:給出a映射到b的函數和裝了a的盒子,結果會返回裝了b的盒子。**fmap** 可以看作是一個接受一個function 和一個 **functor** 的函數,它把function 應用到 **functor** 的每一個元素(映射)。

```haskell
-- Functor的定義
class Functor f where
    fmap :: (a -> b) -> f a -> f b
```
<!-- more -->

入口

使用node進行文件操作,然後調用解析器生成json文件

const { readdirSync, readFileSync, writeFile } = require("fs");
const path = require("path");
const parse = require("./parse");

const files = readdirSync(path.join(__dirname, "posts"));
for (let p of files) {
  let md = readFileSync(path.join(__dirname, "posts", p));
  const objs = parse(md);
  writeFile(path.join(__dirname, "json", p.replace('.md','.json')), JSON.stringify(objs), function( err ){
    err && console.log(err);
  });
}

來看一下解析器入口部分,主要分為:summary 部分,code代碼部分,markdown文本部分。將文本內容的註釋和空格過濾掉,但是代碼部分的註釋要保留。

module.exports = function analyze(str) {
    let ret = { summary: {}, lines: [] };
    while (str) {
        // 空格
        if (/^([\s\t\r\n]+)/.test(str)) {
            str = RegExp.rightContext;
        }
        // summary 內容塊
        if (/^(\-{3})[\r\n]?([\s\S]+?)\1[\r\n]?/.test(str)) {
            str = RegExp.rightContext;
            ret.summary = summaryParse(RegExp.$2);
            ret.num = new Date(ret.summary.date).getTime();
        }
        // code
        if (/^`{3}(\w+)?([\s\S]+?)`{3}/.test(str)) {
            const codeStr = RegExp.$2 || RegExp.$1;
            const fn = (RegExp.$2 && codeParse[RegExp.$1]) ? codeParse[RegExp.$1] : codeParse.javascript;
            str = RegExp.rightContext;
            ret.lines.push({ type: "code", child: fn(codeStr) });
        }
        // 註釋行
        if (/^<!--[\s\S]*?-->/.test(str)) {
            str = RegExp.rightContext;
        }
        // 提取每行字元串, 利用 . 不匹配換行符的特性
        if (/^(.+)[\r\n]?/.test(str)) {
            str = RegExp.rightContext;
            ret.lines.push(textParse(RegExp.$1));
        }
    }
    return ret;
};

文本內容提取

summary 內容塊的提取比較簡單,不講敘。還是看 markdown 文本內容的解析吧。這裡匹配 markdown 常用類型,比如列表,標題h,鏈接a,圖片img等。而返回結果的數據結構就是一個列表,列表裡面可以嵌套子列表。但基本就是正則表達式提取內容,最終消耗完字元行。

function textParse(s) {
    const trans = /^\\(\S)/; //轉義字元
    const italy = /^(\*)(.+?)\1/; //傾斜
    const bold = /^(\*{2})(.+?)\1/; //加粗
    const italyBold = /^(\*{3})(.+?)\1/; //傾斜和加粗
    const headLine = /^(\#{1,6})\s+/; //h1-6
    const unsortList = /^([*\-+])\s+/; //無序列表
    const sortList = /^(\d+)\.\s+/; //有序列表
    const link = /^\*?\[(.+)\]\(([^()]+)\)\*?/; //鏈接
    const img = /^(?:!\[([^\]]+)\]\(([^)]+)\)|<img(\s+)src="([^"]+)")/; //圖片
    const text =/^[^\\\s*]+/; //普通文本

    if (headLine.test(s)) return { type: "h" + RegExp.$1.length, text: RegExp.rightContext };
    if (sortList.test(s)) return { type: "sl", num: RegExp.$1, child: lineParse(RegExp.rightContext) };
    if (unsortList.test(s)) return { type: "ul", num: RegExp.$1, child: lineParse(RegExp.rightContext) };
    if (img.test(s)) return { type: "img", src: RegExp.$2||RegExp.$4, alt: RegExp.$1||RegExp.$3 };
    if (link.test(s)) return { type: "link", href: RegExp.$2, text: RegExp.$1 };
    return { type: "text", child: lineParse(s) };

    function lineParse(line) {
        let ws = [];
        while (line) {
            if (/^[\s]+/.test(line)) {
                ws.push({ type: "text", text: "&nbsp;" });
                line = RegExp.rightContext;
            }
            if (trans.test(line)) {
                ws.push({ type: "text", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (sortList.test(line)) {
                return { child: lineParse(RegExp.rightContext) };
            }
            if (unsortList.test(line)) {
                return { child: lineParse(RegExp.rightContext) };
            }
            if (link.test(line)) {
                ws.push({ type: "link", href: RegExp.$2, text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (italyBold.test(line)) {
                ws.push({ type: "italybold", text: RegExp.$2 });
                line = RegExp.rightContext;
            }
            if (bold.test(line)) {
                ws.push({ type: "bold", text: RegExp.$2 });
                line = RegExp.rightContext;
            }
            if (italy.test(line)) {
                ws.push({ type: "italy", text: RegExp.$2 });
                line = RegExp.rightContext;
            }
            if (text.test(line)) {
                ws.push({ type: "text", text: RegExp.lastMatch });
                line = RegExp.rightContext;
            }
        }
        return ws;
    }
}

代碼塊顯示

如果只是解析文本內容,還是非常簡單的,但是技術博客嘛,代碼塊是少不了的。為了代碼關鍵字元的顏色顯示效果,為了方便閱讀,還得繼續解析。我博客目前使用到的語言,基本寫了對應的解析器,其實有些解析器是可以共用的,比如 style方法不僅可應用到 css 上, 還可以應用到類似的預解析器上比如:scsslesshtml也一樣可應用到類似的標記語言上。

const codeParse = {
  haskell(str){},
  javascript(str){},
  html:html,
  css:style
};

來看一下比較有代表性的 JavaScript 解析器,這裡沒有使用根據換行符(\n)將文本內容切割成字元串數組的方式,因為有些類型需要跨行進行聯合推斷,比如解析塊,方法名稱判斷就是如此。只能將一整塊文本用正則表達式慢慢匹配消耗完。最終的結果類似上面的文本匹配結果 - 嵌套列表,類型就是語法關鍵字,常用內置方法,字元串,數字,特殊符號等。

其實根據這個解析器可以進一步擴展和抽象一下,將它作為類 C 語言族的基本框架。然後只要傳遞 對應語言的正則表達式規則,就能解析出不同語言的結果出來,比如 C#javaC++GO

javascript(str) {
    const comReg = /^\/{2,}.*/;
    const keyReg = /^(import|from|extends|new|var|let|const|return|if|else|switch|case|break|continue|of|for|in|Array|Object|Number|Boolean|String|RegExp|Date|Error|undefined|null|true|false|this|alert|console)(?=([\s.,;(]|$))/;
    const typeReg = /^(window|document|location|sessionStorage|localStorage|Math|this)(?=[,.;\s])/;
    const regReg = /^\/\S+\/[gimuys]?/;
    const sysfunReg = /^(forEach|map|filter|reduce|some|every|splice|slice|split|shift|unshift|push|pop|substr|substring|call|apply|bind|match|exec|test|search|replace)(?=[\s\(])/;
    const funReg = /^(function|class)\s+(\w+)(?=[\s({])/;
    const methodReg = /^(\w+?)\s*?(\([^()]*\)\s*?{)/;
    const symbolReg = /^([!><?|\^$&~%*/+\-]+)/;
    const strReg = /^([`'"])([^\1]*?)\1/;
    const numReg = /^(\d+\.\d+|\d+)(?!\w)/;
    const parseComment = s => {
        const ret = [];
        const lines = s.split(/[\r\n]/g);
        for (let line of lines) {
            ret.push({ type: "comm", text: line });
        }
        return ret;
    };

    let ret = [];

    while (str) {
        if (/^\s*\/\*([\s\S]+?)\*\//.test(str)) {
            str = RegExp.rightContext;
            const coms = parseComment(RegExp.lastMatch);
            ret = ret.concat(coms);
        }
        if (/^(?!\/\*).+/.test(str)) {
            str = RegExp.rightContext;
            ret.push({ type: "text", child:lineParse(RegExp.lastMatch) });
        }
        if(/^[\r\n]+/.test(str)){
            str=RegExp.rightContext;
            ret.push({type:'text',text:RegExp.lastMatch});
        }
    }
    return ret;

    function lineParse(line) {
        let ws = [];
        while (line) {
            if (/^([\s\t\r\n]+)/.test(line)) {
                ws.push({ type: "text", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (comReg.test(line)) {
                ws.push({ type: "comm", text: line });
                break;
            }
            if (regReg.test(line)) {
                ws.push({ type: "fun", text: RegExp.lastMatch });
                line = RegExp.rightContext;
            }
            if (symbolReg.test(line)) {
                ws.push({ type: "keyword", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (keyReg.test(line)) {
                ws.push({ type: "keyword", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (funReg.test(line)) {
                ws.push({ type: "keyword", text: RegExp.$1 });
                ws.push({ type: "text", text: "&nbsp;" });
                ws.push({ type: "fun", text: RegExp.$2 });
                line = RegExp.rightContext;
            }
            if (methodReg.test(line)) {
                ws.push({ type: "fun", text: RegExp.$1 });
                ws.push({ type: "text", text: "&nbsp;" });
                ws.push({ type: "text", text: RegExp.$2 });
                line = RegExp.rightContext;
            }
            if (typeReg.test(line)) {
                ws.push({ type: "fun", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (sysfunReg.test(line)) {
                ws.push({ type: "var", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (strReg.test(line)) {
                ws.push({ type: "var", text: RegExp.$1 + RegExp.$2 + RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (numReg.test(line)) {
                ws.push({ type: "var", text: RegExp.$1 });
                line = RegExp.rightContext;
            }
            if (/^\w+/.test(line)) {
                ws.push({ type: "text", text: RegExp.lastMatch });
                line = RegExp.rightContext;
            }
            if (/^[^`'"!><?|\^$&~%*/+\-\w]+/.test(line)) {
                ws.push({ type: "text", text: RegExp.lastMatch });
                line = RegExp.rightContext;
            }
        }
        return ws;
    }
}

顯示WXML

最後只要運行解析器,就能生成 markdown 對應的 json 文件了,然後把json載入到微信小程式的雲資料庫裡面,剩下的顯示就交由小程式完成。下麵就是使用 taro 編寫 jsx 顯示部分

<View className='article'>
    {lines.map(l => (
        <Block>
        <View className='line'>
            {l.type.search("h") == 0 && ( <Text className={l.type}>{l.text}</Text> )}
            {l.type == "link" && ( <Navigator className='link' url={l.href}> {l.text} </Navigator> )}
            {l.type == "img" && ( <Image className='pic' mode='widthFix' src={l.src} /> )}
            {l.type == "sl" && ( <Block> 
                <Text decode className='num'> {l.num}.{" "} </Text>
                <TextChild list={l.child} />
            </Block>
            )}
            {l.type == "ul" && ( <Block> 
                <Text decode className='num'> {" "} &bull;{" "} </Text>
                <TextChild list={l.child} />
            </Block>
            )}
            {l.type == "text" && l.child.length && ( <TextChild list={l.child} /> )}
        </View>
        {l.type == "code" && (
            <View className='code'>
            {l.child.map(c => (
                <View className='code-line'>
                {c.type == 'comm' && <Text decode className='comm'> {c.text} </Text>}
                {c.type == 'text' && c.child.map(i => (
                    <Block>
                    {i.type == "comm" && ( <Text decode className='comm'> {i.text} </Text> )}
                    {i.type == "keyword" && ( <Text decode className='keyword'> {i.text} </Text> )}
                    {i.type == "var" && ( <Text decode className='var'> {i.text} </Text> )}
                    {i.type == "fun" && ( <Text decode className='fun'> {i.text} </Text> )}
                    {i.type == "text" && ( <Text decode className='text'> {i.text} </Text> )}
                    </Block>
                ))}
                </View>
            ))}
            </View>
        )}
        </Block>
    ))}
</View>

後記

經過這個項目的磨練,我的正則表達式的能力又上了一個臺階, 連 環視 都已經是信手拈來了

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

-Advertisement-
Play Games
更多相關文章
  • 作為一個前端新人,學習了設計模式以後,希望能從源頭上,用淺顯易懂的語言來解釋它。當然不一定是正確的,只是我個人對設計模式的一點淺顯理解。 創建型設計模式 創建型設計模式:故名思意,這些模式都是用來創建實例對象的。 單例模式:首先我們需要理解什麼是單例。單:指的是一個。例:指的是創建的實例。單例:指的 ...
  • js 函數式編程 定義把函數作為第一類對象是函數式編程的第一步,函數式編程是一種編碼風格,他通過書寫函數式代碼來解決問題(而不是一系列執行步驟,就像 就像那種更主流的命令式編程),函數式編程可以讓代碼更容易測試、擴展、及模塊化 1.函數是javascript中的一等公民,(主要是的某個變數可以等於一 ...
  • 觀察者模式 定義 觀察者模式(又被稱為發佈 訂閱(Publish/Subscribe)模式,屬於行為型模式的一種,它定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態變化時,會通知所有的觀察者對象,使他們能夠自動更新自己 註意(觀察者模式和發佈訂閱是有不同的) 區 ...
  • 關於自學WEB前端能不能通過社招找到一份互聯網公司WEB前端開發的工作,有無數的人問出這樣的問題,答案沒有標準的,只能從概率去考慮。有的人可以,有的人不可以,有的人自學就業的概率就是高,有的概率就是低。 比如一本院校學習通信的學生,他肯定是學過編程的,學習能力也不錯。他學成的概率就很高。 比如一個初 ...
  • 首先,我們要知道如何得到屏幕的相關數據。 <html><head><title>獲取當前對象大小以及屏幕解析度等</title><body><div style=" width:88%;margin:30px auto; color:blue;" id="div_html"></div><scrip ...
  • 思路=》 用div來展示刮獎結果,用canvas繪製刮獎前展示的圖片或者文字;將canvas疊在div上方,刮獎是只需要操作canvas配合touch事件即可簡單完成。 canvas刮獎可以用globalCompositeOperation屬性製作。 globalCompositeOperation ...
  • 解釋幾個概念: 替換元素:官方解釋是,一個內容不受css視覺格式化模型控制,css渲染模型並不考慮對此內容的渲染,且元素一般擁有固定的尺寸,(高度,寬度)的元素,被稱為置換元素。通俗來說就是瀏覽器根據元素的標簽和屬性,來決定元素的具體顯示內容。 舉例來說:瀏覽器會根據img標簽的src屬性的值來讀取 ...
  • 一、單頁面應用(SPA) 1.1 C/S到B/S頁面架構的轉變 C/S:客戶端/伺服器(Client/Server)架構的軟體。 C/S 軟體的特點: ① 從window桌面雙擊打開 ② 更新的時候會替換原有的,原有的必須刪除,不能刷新。 ③ 在其他的設備上使用的時候,也要安裝軟體。 B/S :瀏覽 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...