由于打算使用Python作为前端语言替换JavaScript,所以先看看哪个插件实现起来最顺眼。
本次参与的插件有:(点击可跳转github代码仓库)
由于PyScript依赖于Pyodide,所以此处就不再选用Pyodide了。
在浏览器中运行Python代码主要有两种实现方式:编译运行和内置Python解释器。
Brython和Skulpt就是通过将Python语言编译成JavaScript,然后在浏览器网页中运行。
Pyodide和PyPy.js
需要引入的脚本(cdn):
- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/brython@3.10.7/brython.min.js"></script>
- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/brython@3.10.7/brython_stdlib.js"></script>
-
可能引入会有点慢,我这使用HbuilderX自带服务器打开用时接近1分钟!
我们可以下载后加载,这样加载速度会快不少:
现在是毫秒级响应了。
这个库的使用不做太多示范,这里就提出偶然发现的不足:无法像真正的Python一样,不满足万物皆对象,或者里面的isinstance方法无法判断父类,导致判断的时候判断死了,示例代码:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>Brython</title>
- <!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/brython@3.10.7/brython.min.js"></script> -->
- <!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/brython@3.10.7/brython_stdlib.js"></script> -->
- <script type="text/javascript" src="js/brython/brython.min.js"></script>
- <script type="text/javascript" src="js/brython/brython_stdlib.js"></script>
- </head>
- <body onload="brython()">
- <input id="zone"><button id="mybutton">click !</button>
- <script type="text/python">
- from browser import document, alert, console
- from string import ascii_letters
-
- _str = str
-
-
- class str(_str):
- __metaclass__ = _str
-
- @staticmethod
- def object2str(obj: (_str, int, bool, list, tuple, dict, set), skip_key: (list, tuple) = None):
- string = []
- if isinstance(obj, (list, tuple)):
- for o in obj:
- string.append(str.object2str(obj=o, skip_key=skip_key))
- string = f'[{", ".join(string)}]'
- elif isinstance(obj, set):
- for o in obj:
- string.append(str.object2str(obj=o, skip_key=skip_key))
- string = f'{{{", ".join(string)}}}'
- elif isinstance(obj, dict):
- for o in obj:
- if not (isinstance(skip_key, (list, tuple)) and o in skip_key):
- string.append(f'{str.object2str(obj=o, skip_key=skip_key)}: {str.object2str(obj=obj[o], skip_key=skip_key)}')
- string = f'{{{", ".join(string)}}}'
- # elif isinstance(obj, (_str, int, bool)):
- else:
- string = repr(obj)
- return string
-
- def __init__(self, o: (object, bytes), encoding: _str = 'utf-8', errors: _str = 'strict'):
- if isinstance(o, bytes):
- self.__string = _str(o, encoding, errors)
- elif isinstance(o, (_str, str, int, float, bool, list, set, tuple, object)):
- self.__string = _str(o) # self.object2str(o, skip_key=__import__('sys').argv.pop() if __import__('sys').argv else None)
- else:
- raise TypeError("Attribute o must be <class 'object'> or <class 'bytes'>")
-
- def __str__(self):
- return self.__string
-
- def __repr__(self):
- return self.__string
-
- def __setitem__(self, key: (int, slice), value: (object, bytes)):
- value = str(value)
- old = self.__string
- if isinstance(key, int):
- assert key < len(old), IndexError('string index out of range')
- assert len(value) == 1, ValueError('value length must be 1')
- self.__string = old[: key] + value + old[key + 1:]
- elif isinstance(key, slice):
- arange = list(range((key.start or 0), (key.stop or len(old)), (key.step or 1)))
- assert max(arange) < len(old) and min(arange) >= 0, IndexError('string index out of range')
- assert len(value) == len(arange), ValueError('length must be equal')
- for index, item in zip(arange, value):
- self.__string = old[: index] + item + old[index + 1:]
- old = self.__string
-
-
- def echo(event):
- print(event.__dict__)
- console.log(document["zone"].value)
- alert(document["zone"].value)
-
- document["mybutton"].bind("click", echo)
-
-
- love = str('love')
- love[2] = str('w')
- love[1] = 'p'
- print(love)
- a_Z = str(ascii_letters)
- print(a_Z)
- a_Z[::2] = a_Z[::2].swapcase()
- print(a_Z)
- print(a_Z.upper())
- num = str(1234)
- print(num)
- num[2:] = 48
- print(num)
- print(str.object2str({'a': {'b': 'c', 'c': 'd', 'd': 'e'}, 'c': 'e'}, ['c']))
- </script>
- </body>
- </html>
-
直接执行会报错:
而把object改成可能出现的各种基础数据类型((_str, str, int, float, bool, list, set, tuple, object)·)的话,明显程序就正常运行了,或者这里直接跳过一个elif直接else后跟_str(o)也可以解决这个问题。
可能没明白这打印的啥,其实我就是实现了一个简易的可变字符串,为了实现而实现,不要用于实际使用,性能会比较差。
代码中还包括了一个官网的例子,我又加了一个输出,结果如下:
可以发现打印出来的属性好像都是JavaScript的属性。
这个我看有个网站就是用这个做的,所以我肯定不会再去写了,既可以画图,又可以输出文字,加载速度还很快,很不错!
至于代码,还是复制粘贴为妙,因为似乎有那么一点点的小复杂:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>Skulpt</title>
- <script type="text/javascript" src="js/skulpt/skulpt.min.js"></script>
- <script type="text/javascript" src="js/skulpt/skulpt-stdlib.js"></script>
- <link href="js/skulpt/codemirror.css" rel="stylesheet" type="text/css">
- <link href="js/skulpt/monokai.css" rel="stylesheet" type="text/css">
- <link href="js/skulpt/fullscreen.css" rel="stylesheet" type="text/css">
- <script src="js/skulpt/codemirror.js"></script>
- <script src="js/skulpt/python.js"></script>
- <link href="js/skulpt/index.css" rel="stylesheet" type="text/css">
- <link rel="stylesheet" type="text/css" href="js/skulpt/iview.css">
- <script type="text/javascript" src="js/skulpt/vue.js"></script>
- <script type="text/javascript" src="js/skulpt/iview.min.js"></script>
- </head>
- <body>
- <div id="app">
- <div class="function">
- <i-button type="primary" icon="md-play" onclick="runit()">运行</i-button>
-
- <i-button type="error" icon="ios-trash" @click="clear">清除所有</i-button>
- </div>
- <div class="page">
- <div css="workbench">
- <form class="index-form">
- <textarea id="yourcode" class="index-form">
- import turtle
- turtle.begin_fill()
- turtle.fillcolor('red')
- for x in range(4):
- turtle.forward(100)
- turtle.right(90)
- turtle.end_fill()
- turtle.hideturtle()
- turtle.done()
- print("Hello World")
- </textarea>
- </form>
- <div class="outputd">
- <div id="mycanvas" class="canvas-ouput"></div>
- <div class="output">
- <pre id="output"> </pre>
- </div>
- </div>
- </div>
- </div>
- </div>
- <script>
- this.Sk.configure({
- // 表明现在是python3
- __future__: this.Sk.python3,
- // 输出函数
- output: self.out,
- // 导入模块时调用
- read: self.read,
- // fileopen函数被调用时调用
- fileopen: file => {
- console.log("skulpt file open ", file);
- },
- // filewrite函数被调用时调用
- filewrite: file => {
- console.log("skulpt file write ", file);
- },
- // 调用input指令时调用
- inputfun: self.inputfun,
- inputfunTakesPrompt: true
- });
- new Vue({
- el: '#app',
- data: {},
- methods: {
- clear: function(event) {
- CodeMirrorEditor.setValue("");
- var mypre = document.getElementById("output");
- mypre.innerHTML = "";
- // var canvas = document.getElementById("mycanvas").getElementsByTagName('canvas')[1];
- // canvas.getContext('2d').clearRect(0,0,canvas.width,canvas.height);
- for (let canvas of document.getElementById("mycanvas").getElementsByTagName('canvas')) {
- canvas.width = canvas.width;
- }
- },
- }
- })
- //新增window.onload 事件,用来初始化codemirror
- // window.onload = function () {
- // //console.log("onload is going")
- // // 添加codemirror 模式
- var myTextarea = document.getElementById('yourcode');
- var CodeMirrorEditor = CodeMirror.fromTextArea(myTextarea, {
- mode: "python",
- lineNumbers: true,
- lineWrapping: true,
- foldGutter: true,
- matchBrackets: true,
- autoCloseBrackets: true,
- styleActiveLine: true,
- indentUnit: 4,
- });
- CodeMirrorEditor.setSize("100%", "100%");
- // }
- function outf(text) {
- var mypre = document.getElementById("output");
- mypre.innerHTML = mypre.innerHTML + text;
- }
-
- function builtinRead(x) {
- if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined)
- throw "File not found: '" + x + "'";
- return Sk.builtinFiles["files"][x];
- }
-
- function runit() {
- var mypre = document.getElementById("output");
- mypre.innerHTML = "";
- // var canvas = document.getElementById("mycanvas").getElementsByTagName('canvas')[1];
- // canvas.getContext('2d').clearRect(0,0,canvas.width,canvas.height);
- for (let canvas of document.getElementById("mycanvas").getElementsByTagName('canvas')) {
- canvas.width = canvas.width;
- }
- var prog = CodeMirrorEditor.getValue();
- var mypre = document.getElementById("output");
- mypre.innerHTML = '';
- Sk.pre = "output";
- Sk.configure({
- output: outf,
- read: builtinRead,
- __future__: Sk.python3
- });
- (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'mycanvas';
- var myPromise = Sk.misceval.asyncToPromise(function() {
- return Sk.importMainWithBody("<stdin>", false, prog, true);
- });
- myPromise.then(function(mod) {
- console.log('success');
- },
- function(err) {
- console.log(err.toString());
- mypre.innerHTML = err.toString();
- });
- }
- </script>
- </body>
- </html>
-
如果按示例网站上所引用的话,也要引用一堆:
- <script src="http://python.longkui.site/skulpt.min.js" type="text/javascript"></script>
- <script src="http://python.longkui.site/skulpt-stdlib.js" type="text/javascript"></script>
- <link href="http://python.longkui.site/codemirror/lib/codemirror.css" rel="stylesheet" type="text/css">
- <link href="http://python.longkui.site/codemirror/theme/monokai.css" rel="stylesheet" type="text/css">
- <link href="http://python.longkui.site/codemirror/addon/display/fullscreen.css" rel="stylesheet" type="text/css">
- <script src="http://python.longkui.site/codemirror/lib/codemirror.js"></script>
- <script src="http://python.longkui.site/codemirror/mode/python/python.js"></script>
- <link href="http://python.longkui.site/css/index.css" rel="stylesheet" type="text/css">
- <link rel="stylesheet" type="text/css" href="http://unpkg.com/view-design/dist/styles/iview.css">
- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
- <script type="text/javascript" src="http://unpkg.com/view-design/dist/iview.min.js"></script>
-
当然核心还是skulpt.min.js和skulpt-stdlib.js,其他的都是为了好看的界面。
官方在线体验地址:pypyjs(交互环境)pypyjs(编辑器环境)【他的插件较大,加载较慢】
从官方的在线环境来看,他是一个Python2.7.9的环境,对需要使用Python3环境的人来说可以pass掉了。看代码仓库更新日期,都七八年前了,所以这里也就不体验了,就提一下这个插件。(毕竟和我志不同道不合~)
根据其他有限资料,这里引来一段话:
PyPyJs是在浏览器中JIT编译Python的实现 PyPy的一个可替代品,其使用emscripten。它有潜力能够非常迅速地运行Python代码,原因和PyPy一样。不幸的是,它也像PyPy一样有无法执行C扩展 的问题。所有这些方式使得我们要去重写科学计算工具来获得足够的性能。作为一个经常使用Matplotlib的人,我知道那将占据多少数不清的个人时间:其它的项目已经进行过尝试并停滞不前,而且肯定是会带来许多我们这样的杂凑的创业团队无法处理的工作量。我们因此需要构建一个尽可能地接近标准Python实现的工具,以及大多数据科学家已经使用的科学计算技术栈。
用Python2的同学可以参考官网自己尝试鼓捣鼓捣,相信你一定能研究出来的!
Python3的同学建议使用PyScript(Pyodide)
这个和PyPy.js一样,有个很长的加载前摇,需要loading很长时间,好就好在他可以轻松引入第三方模块,还是以whl的安装包引入,这就很方便。不过他加载的库越多,引入越慢,库多的时候可能要按小时计算。
他有REPL模式,和Jupyter Notebook一样,可以单独运行代码块,每个代码块的变量又都是全局的。
由于pyscript.js脚本加载pyodide.js脚本时是在根上加载(即类似127.0.0.1:80/pyodide.js和www.example.com/pyodide.js)所以建议用vue加载,或者其他web服务器,或者有没有大佬告诉我如何更改使得他的加载路径可以调整,我在HbuilderX上加载就是打开的网址是:http://127.0.0.1:8848/PythonOnline/pyscript.html,但这个文件的加载地址是:http://127.0.0.1:8848/pyodide.js,所以直接404,然后就加载不出来,或者用官网加载,不过得耐得住寂寞:
单单是numpy库就要加载1分钟,总计甚至加载了3.5分钟!才26.4MB的资源而已,库多不知道要加载到什么时候,千兆宽带都不管用。
首先来看REPL模式:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>PyScript</title>
- <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
- <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
- <style>
- kbd {
- padding: 2px 8px;
- border: 1px solid rgba(63, 63, 63, .25);
- box-shadow: 0 1px 0 rgb(63 63 63 / 25%);
- background-color: #fff;
- color: #333;
- border-radius: 4px;
- display: inline-block;
- margin: 0 2px;
- white-space: nowrap;
- }
-
- #output {
- float: left;
- }
-
- #output>div {
- font-family: 'monospace';
- background-color: #e5e5e5;
- border: 1px solid lightgray;
- border-top: 0;
- font-size: 0.875rem;
- padding: 0.5rem;
- }
-
- .my-repl {
- border-top: 1px solid lightgray;
- }
-
- #err-div {
- border: 0;
- }
- </style>
- </head>
- <py-env>
- - numpy
- </py-env>
- <body>
- <h1><b>pyscript REPL</b></h1>
- <div style="border: 1px solid red; padding: 15px; word-wrap: break-word; font-size: 15px;"><b>Installd and loaded:numpy</b></div>
- <br />Tip: press <kbd>Shift</kbd>+<kbd>ENTER</kbd> to evaluate a cell<br />
- <!-- <py-box widths="2/3;1/3"> -->
- <div style="width: 100%;">
- <div class="w-2/3" style="float: left;">
- 固定:<py-repl class="my-repl" std-out="output" std-err="err-div"></py-repl>
- 自增:<py-repl class="my-repl" auto-generate="true" std-out="output" std-err="err-div"></py-repl>
- </div>
- <div id="output" class="w-1/3"></div>
- </div>
- <!-- </py-box> -->
- <footer id="err-div"
- class="bg-red-700 text-white text-center border-t-4 border-green-500 fixed inset-x-0 bottom-0 p-4 hidden">
- </footer>
- </body>
- </html>
-
在网页上就是这样的执行结果(快捷键Shift+ENTER执行):
要是加载了这么久,再返回个报错,是不是会让人崩溃:
现在来试试直接插入脚本:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>PyScript</title>
- <link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
- <script defer src="https://pyscript.net/alpha/pyscript.js"></script>
- </head>
- <body>
- <py-env>
- - paths:
- - /PythonOnline/pys/loop_dump.py
- </py-env>
- <!-- <py-script src="/PythonOnline/pys/loop_dump.py"></py-script> -->
- <py-script>
- from loop_dump import paths, dump
- dump(paths)
- </py-script>
- </body>
- </html>
-
既可以直接写,也可以引入脚本地址,执行结果,完美:
看这文件树,感觉就像是模拟了一个环境,难怪要加载wasm文件。
如果不用第三方模块,可以先在PyCharm里试试,代码正确后移入html中,如果有第三方模块,得看看PyScript支持不支持,在我心中PyScript是加载第三方模块的首选,Skulpt是无第三方模块的首选。