今年國慶假期終于可以憋在家里了不用出門了,不用出去看后腦了,真的是一種享受。這么好的光陰怎么浪費,睡覺、吃飯、打豆豆這怎么可能(耍多了也煩),完全不符合我們程序員的作風,趕緊起來把文章寫完。
這篇文章比較基礎,在國慶期間的業余時間寫的,這幾天又完善了下,力求把更多的前端所涉及到的關于文件上傳的各種場景和應用都涵蓋了,若有疏漏和問題還請留言斧正和補充。
以下是本文所涉及到的知識點,break or continue ?
原理很簡單,就是根據 http 協議的規范和定義,完成請求消息體的封裝和消息體的解析,然后將二進制內容保存到文件。
我們都知道如果要上傳一個文件,需要把 form 標簽的enctype設置為multipart/form-data,同時method必須為post方法。
那么multipart/form-data表示什么呢?
multipart互聯網上的混合資源,就是資源由多種元素組成,form-data表示可以使用HTML Forms 和 POST 方法上傳文件,具體的定義可以參考RFC 7578。
multipart/form-data 結構
看下 http 請求的消息體
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次請求要上傳文件,其中boundary表示分隔符,如果要上傳多個表單項,就要使用boundary分割,每個表單項由———XXX開始,以———XXX結尾。
每一個表單項又由Content-Type和Content-Disposition組成。
Content-Disposition: form-data 為固定值,表示一個表單元素,name 表示表單元素的 名稱,回車換行后面就是name的值,如果是上傳文件就是文件的二進制內容。
Content-Type:表示當前的內容的 MIME 類型,是圖片還是文本還是二進制數據。
解析
客戶端發送請求到服務器后,服務器會收到請求的消息體,然后對消息體進行解析,解析出哪是普通表單哪些是附件。
可能大家馬上能想到通過正則或者字符串處理分割出內容,不過這樣是行不通的,二進制buffer轉化為string,對字符串進行截取后,其索引和字符串是不一致的,所以結果就不會正確,除非上傳的就是字符串。
不過一般情況下不需要自行解析,目前已經有很成熟的三方庫可以使用。
至于如何解析,這個也會占用很大篇幅,后面的文章在詳細說。
使用 form 表單上傳文件
在 ie時代,如果實現一個無刷新的文件上傳那可是費老勁了,大部分都是用 iframe 來實現局部刷新或者使用 flash 插件來搞定,在那個時代 ie 就是最好用的瀏覽器(別無選擇)。
DEMO
這種方式上傳文件,不需要 js ,而且沒有兼容問題,所有瀏覽器都支持,就是體驗很差,導致頁面刷新,頁面其他數據丟失。
HTML
<form method="post" action="http://localhost:8100" enctype="multipart/form-data">
選擇文件:
<input type="file" name="f1"/> input 必須設置 name 屬性,否則數據無法發送<br/>
<br/>
標題:<input type="text" name="title"/><br/><br/><br/>
<button type="submit" id="btn-0">上 傳</button>
</form>
復制代碼
服務端文件的保存基于現有的庫koa-body結合 koa2實現服務端文件的保存和數據的返回。
在項目開發中,文件上傳本身和業務無關,代碼基本上都可通用。
在這里我們使用koa-body庫來實現解析和文件的保存。
koa-body 會自動保存文件到系統臨時目錄下,也可以指定保存的文件路徑。
然后在后續中間件內得到已保存的文件的信息,再做二次處理。
NODE
/**
* 服務入口
*/
var http = require('http');
var koaStatic = require('koa-static');
var path = require('path');
var koaBody = require('koa-body');//文件保存庫
var fs = require('fs');
var Koa = require('koa2');
var app = new Koa();
var port = process.env.PORT || '8100';
var uploadHost= `http://localhost:${port}/uploads/`;
app.use(koaBody({
formidable: {
//設置文件的默認保存目錄,不設置則保存在系統臨時目錄下 os
uploadDir: path.resolve(__dirname, '../static/uploads')
},
multipart: true // 開啟文件上傳,默認是關閉
}));
//開啟靜態文件訪問
app.use(koaStatic(
path.resolve(__dirname, '../static')
));
//文件二次處理,修改名稱
app.use((ctx) => {
var file = ctx.request.files.f1;//得道文件對象
var path = file.path;
var fname = file.name;//原文件名稱
var nextPath = path+fname;
if(file.size>0 && path){
//得到擴展名
var extArr = fname.split('.');
var ext = extArr[extArr.length-1];
var nextPath = path+'.'+ext;
//重命名文件
fs.renameSync(path, nextPath);
}
//以 json 形式輸出上傳文件地址
ctx.body = `{
"fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
}`;
});
/**
* http server
*/
var server = http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ...... ');
復制代碼
CODE
https://github.com/Bigerfe/fe-learn-code/
markdown中寫下你的文章,并使用Python將它們轉換成HTML-作者Florian Dahlitz,于2020年5月18日(15分鐘)
介紹
幾個月前,我想開通自己的博客,而不是使用像Medium這樣的網站。這是一個非常基礎的博客,所有的文章都是HTML形式的。然而,有一天,我突然產生了自己編寫Markdown到HTML生成器的想法,最終這將允許我用markdown來編寫文章。此外,為它添加諸如估計閱讀時間之類的擴展特性會更容易。長話短說,我實現了自己的markdown到HTML生成器,我真的很喜歡它!
在本系列文章中,我想向您展示如何構建自己的markdown到HTML生成器。該系列由三部分組成:
第一部分(本文)介紹了整個管線的實現。
第二部分通過一個模塊擴展了實現的管線,該模塊用于計算給定文章的預計閱讀時間。
第三部分演示如何使用管線生成自己的RSS摘要。
這三部分中使用的代碼都可以在GitHub上找到。
備注:我的文章中markdown到HTML生成器的想法基于Anthony Shaw文章中的實現。
項目構建
為了遵循本文的內容,您需要安裝幾個軟件包。我們把它們放進requirements.txt文件。
Markdown是一個包,它允許您將markdown代碼轉換為HTML。之后我們用Flask產生靜態文件。
但在安裝之前,請創建一個虛擬環境,以避免Python安裝出現問題:
激活后,您可以使用pip安裝requirements.txt中的依賴。
很好!讓我們創建幾個目錄來更好地組織代碼。首先,我們創建一個app目錄。此目錄包含我們提供博客服務的Flask應用程序。所有后續目錄都將在app目錄內創建。其次,我們創建一個名為posts的目錄。此目錄包含要轉換為HTML文件的markdown文件。接下來,我們創建一個templates目錄,其中包含稍后使用Flask展示的模板。在templates目錄中,我們再創建兩個目錄:
posts包含生成的HTML文件,這些文件與應用程序根目錄中posts目錄中的文件相對應。
shared包含在多個文件中使用的HTML文件。
此外,我們還創建了一個名為services的目錄。該目錄將包含我們在Flask應用程序中使用的模塊,或者為它生成某些東西。最后,創建一個名為static的目錄帶有兩個子目錄images和css。自定義CSS文件和文章的縮略圖將存儲在此處。
您的最終項目結構應如下所示:
令人驚嘆!我們完成了一般的項目設置。我們來看看Flask的設置。
Flask設置
路由
我們在上一節安裝了Flask。但是,我們仍然需要一個Python文件來定義用戶可以訪問的端點。在app目錄中創建main.py并將以下內容復制到其中。
該文件定義了一個具有兩個端點的基礎版Flask應用程序。用戶可以使用/route訪問第一個端點返回索引頁,其中列出了所有文章。
第二個端點是更通用的端點。它接受post的名稱并返回相應的HTML文件。
接下來,我們通過向app目錄中添加一個__init__.py,將其轉換為一個Python包。此文件為空。如果您使用UNIX計算機,則可以從項目的根目錄運行以下命令:
模板
現在,我們創建兩個模板文件index.html以及layout.html,都存儲在templates/shared目錄中。這個layout.html模板將用于單個博客條目,而index.html模板用于生成索引頁,從中我們可以訪問每個帖子。讓我們從index.html模板開始。
它是一個基本的HTML文件,其中有兩個元標記、一個標題和兩個樣式表。注意,我們使用一個遠程樣式表和一個本地樣式表。遠程樣式表用于啟用Bootstrap[1]類。第二個是自定義樣式。我們晚點再定義它們。
HTML文件的主體包含一個容器,其中包含Jinja2[2]邏輯,用于為每個post生成Bootstrap卡片[3]。您是否注意到我們不直接基于變量名訪問這些值,而是需要將[0]添加到其中?這是因為文章中解析的元數據是列表。實際上,每個元數據元素都是由單一元素組成的列表。我們稍后再看。到目前為止,還不錯。讓我們看看layout.html模板。
如你所見,它比前一個短一點,簡單一點。文件頭與index.html文件很相似,除了我們有不同的標題。當然,我們可以共用一個模板,但是我不想讓事情變得更復雜。
body中的容器僅定義一個h1標記。然后,我們提供給模板的內容被插入并呈現。
樣式
正如上一節所承諾的,我們將查看自定義CSS文件style.css. 我們在static/css中找到該文件,并根據需要自定義頁面。下面是我們將用于基礎示例的內容:
我不喜歡Bootstrap中blockquotes的默認外觀,所以我們在左側添加了一點間距和邊框。此外,blockquote段落底部的頁邊空白將被刪除。不刪除的話看起來很不自然。
最后但并非最不重要的是,左右兩邊的填充被刪除。由于兩邊都有額外的填充,縮略圖沒有正確對齊,所以在這里刪除它們。
到現在為止,一直都還不錯。我們完成了關于Flask的所有工作。讓我們開始寫一些帖子吧!
寫文章
正如標題所承諾的,你可以用markdown寫文章-是的!在寫文章的時候,除了保證正確的markdown格式外,沒有其他需要注意的事情。
在完成本文之后,我們需要在文章中添加一些元數據。此元數據添加在文章之前,并由三個破折號分隔開來---。下面是一個示例文章(post1.md)的摘錄:
注意:您可以在GitHub庫的app/posts/post1.md中找到完整的示例文章。
在我們的例子中,元數據由標題、副標題、類別、發布日期和index.html中卡片對應縮略圖的路徑組成.
我們在HTML文件中使用了元數據,你還記得嗎?元數據規范必須是有效的YAML。示例形式是鍵后面跟著一個冒號和值。最后,冒號后面的值是列表中的第一個也是唯一的元素。這就是我們通過模板中的索引運算符訪問這些值的原因。
假設我們寫完了文章。在我們可以開始轉換之前,還有一件事要做:我們需要為我們的帖子生成縮略圖!為了讓事情更簡單,只需從你的電腦或網絡上隨機選取一張圖片,命名它為placeholder.jpg并把它放到static/images目錄中。GitHub存儲庫中兩篇文章的元數據包含一個代表圖像的鍵值對,值是placeholder.jpg。
注意:在GitHub存儲庫中,您可以找到我提到的兩篇示例文章。
markdown到HTML轉換器
最后,我們可以開始實現markdown to HTML轉換器。因此,我們使用我們在開始時安裝的第三方包Markdown。我們先創建一個新模塊,轉換服務將在其中運行。因此,我們在service目錄中創建了converter.py。我們一步一步看完整個腳本。您可以在GitHub存儲庫中一次查看整個腳本。
首先,我們導入所需的所有內容并創建幾個常量:
ROOT指向我們項目的根。因此,它是包含app的目錄。
POSTS_DIR是以markdown編寫的文章的路徑。
TEMPLATE_DIR分別指向對應的templates目錄。
BLOG_TEMPLATE_文件存儲layout.html的路徑。
INDEX_TEMPLATE_FILE是index.html
BASE_URL是我們項目的默認地址,例如。https://florian-dahlitz.de.默認值(如果不是通過環境變量DOMAIN提供的話)是http://0.0.0.0:5000。
接下來,我們創建一個名為generate_entries的新函數。這是我們定義的唯一一個轉換文章的函數。
在函數中,我們首先獲取POSTS_DIR目錄中所有markdown文件的路徑。pathlib的awesome glob函數幫助我們實現它。
此外,我們定義了Markdown包需要使用的擴展。默認情況下,本文中使用的所有擴展都隨它的安裝一起提供。
注意:您可以在文檔[4]中找到有關擴展的更多信息。
此外,我們實例化了一個新的文件加載程序,并創建了一個在轉換項目時使用的環境。隨后,將創建一個名為all_posts的空列表。此列表將包含我們處理后的所有帖子。現在,我們進入for循環并遍歷POSTS_DIR中找到的所有文章。
我們啟動for循環,并打印當前正在處理的post的路徑。如果有什么東西出問題了,這尤其有用。然后我們就知道,哪個文章的轉換失敗了。
接下來,我們在默認url之后增加一部分。假設我們有一篇標題為“面向初學者的Python”的文章。我們將文章存儲在一個名為python-for-beginners.md,的文件中,因此生成的url將是http://0.0.0.0:5000/posts/python-for-beginners。
變量url_html存儲的字符串與url相同,只是我們在末尾添加了.html。我們使用此變量定義另一個稱為target_file.的變量。變量指向存儲相應HTML文件的位置。
最后,我們定義了一個變量md,它表示markdown.Markdown的實例,用于將markdown代碼轉換為HTML。您可能會問自己,為什么我們沒有在for循環之前實例化這個實例,而是在內部實例化。當然,對于我們這里的小例子來說,這沒有什么區別(只是執行時間稍微短一點)。但是,如果使用諸如腳注之類的擴展來使用腳注,則需要為每個帖子實例化一個新實例,因為腳注添加后就不會從此實例中刪除。因此,如果您的第一篇文章使用了一些腳注,那么即使您沒有明確定義它們,所有其他文章也將具有相同的腳注。
讓我們轉到for循環中的第一個with代碼塊。
實際上,with代碼塊打開當前post并將其內容讀入變量content。之后調用_md.convert將以markdown方式寫入的內容轉換為HTML。隨后,env環境根據提供的模板BLOG_TEMPLATE_FILE(即layout.html如果你還記得的話)渲染生成的HTML。
第二個with 代碼塊用于將第一個with 代碼塊中創建的文檔寫入目標文件。
以下三行代碼從元數據中獲取發布日期(被發布的日期),將其轉換為正確的格式(RFC 2822),并將其分配回文章的元數據。此外,生成的post_dict被添加到all_posts列表中。
我們現在出了for循環,因此,我們遍歷了posts目錄中找到的所有posts并對其進行了處理。讓我們看看generate_entries函數中剩下的三行代碼。
我們按日期倒序對文章進行排序,所以首先顯示最新的文章。隨后,我們將文章寫到模板目錄一個新創建的index.html文件中。別把index.html錯認為templates/shared目錄中的那個。templates/shared目錄中的是模板,這個是我們要使用Flask服務的生成的。
最后我們在函數generate_entries之后添加以下if語句。
這意味著如果我們通過命令行執行文件,它將調用generate_entries函數。
太棒了,我們完成了converter.py腳本!讓我們從項目的根目錄運行以下命令來嘗試:
您應該看到一些正在轉換的文件的路徑。假設您編寫了兩篇文章或使用了GitHub存儲庫中的兩篇文章,那么您應該在templates目錄中找到三個新創建的文件。首先是index.html,它直接位于templates目錄中,其次是templates/posts目錄中的兩個HTML文件,它們對應于markdown文件。
最后啟動Flask應用程序并轉到http://0.0.0.0:5000。
總結
太棒了,你完成了這個系列的第一部分!在本文中,您已經學習了如何利用Markdown包創建自己的Markdown to HTML生成器。您實現了整個管線,它是高度可擴展的,您將在接下來的文章中看到這一點。
希望你喜歡這篇文章。一定要和你的朋友和同事分享。如果你還沒有,考慮在Twitter上關注我@DahlitzF或者訂閱我的通知,這樣你就不會錯過任何即將發表的文章。保持好奇心,不斷編碼!
參考文獻
Bootstrap (http://getbootstrap.com/)
Primer on Jinja Templating (https://realpython.com/primer-on-jinja-templating/)
Bootstrap Card (https://getbootstrap.com/docs/4.4/components/card/)
Python-Markdown Extensions (https://python-markdown.github.io/extensions/)
Tweet
英文原文:https://florian-dahlitz.de/blog/build-a-markdown-to-html-conversion-pipeline-using-python
譯者:阿布銩
了執行Javascript,需要在HTML文件內以特定的方式書寫JavaScript的代碼,JavaScript的書寫方法有多種,其執行的流程也各不相同:
此種嵌入方法無法操作<script>之后的DOM元素。因為<script>之后的DOM元素還未構造,因此在<script>標簽內就無法取得位于其后的DOM元素。
此種嵌入方法可以指定defer、async屬性。defer可以推遲執行,async可以異步執行。
此種嵌入方法在頁面讀取完后再對其執行,所以可以對所有的DOM元素操作。
<body onload="alert('hello')">
window.onload = function(){alert('hello');};
當window.onload事件觸發時,頁面上所有的DOM、樣式表、腳本、圖片、flash都已經加載完成了。
//window.onload不能同時編寫多個。
//以下代碼無法正確執行,結果只輸出第二個。
window.onload = function(){
alert("test1");
};
window.onload = function(){
alert("test2");
};
//$(document).ready()能同時編寫多個
//結果兩次都輸出
$(document).ready(function(){
alert("Hello World");
});
$(document).ready(function(){
alert("Hello again");
});
window.onload和body中onload也有些許區別:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.9.0.js"></script>
<script language="javascript">
window.onload = haha;
function haha(){console.log("window.onload");}
if(document.addEventListener){
function DOMContentLoaded(){
console.log("DOMContentLoaded");
}
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}</script>
</head>
<body onload="console.log('bodyonload');">
<div id="div1">a</div>
</body>
</html>
在IE10和FireFox下,結果為 :
"DOMContentLoaded"
"bodyonload"
說明body中的onload會覆蓋window.onload
在chrome下,結果為:
DOMContentLoaded
window.onload
bodyonload
然后,如果把javascript代碼移到最下面,結果又會是什么樣呢?
chrome和IE10、FireFox的結果竟然是一樣的:
DOMContentLoaded
window.onload
IE 10、Fire Fox可以理解,window.on load和body中的 on load 誰在下面就是誰覆蓋誰,只會執行后面的那個。
onload方法可能需要等待時間,而本方法可以在完成HTML解析后發生的事件,減少等待時間。
在chrome、IE10和FireFox中,執行結果是:DOMContentLoaded然后才是onload的輸出。所以說一般情況下,DOMContentLoaded事件要在window.onload之前執行,當DOM樹構建完成的時候就會執行DOMContentLoaded事件。
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
<script type="text/javascript" src="jquery2.js"></script>
<script language="javascript">
window.onload = haha;
function haha(){console.log(document.getElementById("div1"));}
if(document.addEventListener){
function DOMContentLoaded(){
console.log("DOMContentLoaded");
}
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
}
</script>
</head>
<body>
<div id="div1">a</div>
</body>
</html>
如果你是個jQuery使用者,你可能會經常使用$(document).ready();或者$(function(){}),這都是使用了DOMContentLoaded事件
5.1 使用原生js方法
動態創建script標簽,并指定script的src屬性
function loadJs(url, callback) {
var script = document.createElement('script');
script.type = "text/javascript";
if (typeof(callback) != "undefined") {
if (script.readyState) {
script.onreadystatechange = function() {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
}
} else {
script.onload = function() {
callback();
}
}
}
script.src = url;
document.body.appendChild(script);
}
loadJs("test.js", function() {
alert('done');
});
還可以使用同樣的原理動態加載css文件,只不過插入的的父節點是head標簽。
5.2 使用document.write/writeln()方式
該種方式可以實現js文件的動態加載,原理就是在重寫文檔流,這種方式會導致整個頁面重繪。
document.writeln("<script src=\"http://lib.sinaapp.com/js/jquery/1.6/jquery.min.js\"></script>");
需要注意的是特殊字符的轉義。
5.3 使用jQuery
使用getScript(url,callback)方法實現動態加載js文件
$.getScript('test.js',function(){
alert('done');
});
-End-
*請認真填寫需求信息,我們會在24小時內與您取得聯系。