2018年7月

构建单页面应用(SPA)有许多的框架/库,但是我希望它们能少一些。我有一个解决方案,我想共享给大家。

class Router {
    constructor() {
        this.routes = []
    }

    handle(pattern, handler) {
        this.routes.push({ pattern, handler })
    }

    exec(pathname) {
        for (const route of this.routes) {
            if (typeof route.pattern === 'string') {
                if (route.pattern === pathname) {
                    return route.handler()
                }
            } else if (route.pattern instanceof RegExp) {
                const result = pathname.match(route.pattern)
                if (result !== null) {
                    const params = result.slice(1).map(decodeURIComponent)
                    return route.handler(...params)
                }
            }
        }
    }
}
const router = new Router()

router.handle('/', homePage)
router.handle(/^\/users\/([^\/]+)$/, userPage)
router.handle(/^\//, notFoundPage)

function homePage() {
    return 'home page'
}

function userPage(username) {
    return `${username}'s page`
}

function notFoundPage() {
    return 'not found page'
}

console.log(router.exec('/')) // home page
console.log(router.exec('/users/john')) // john's page
console.log(router.exec('/foo')) // not found page

使用它你可以为一个 URL 模式添加处理程序。这个模式可能是一个简单的字符串或一个正则表达式。使用一个字符串将精确匹配它,但是如果使用一个正则表达式将允许你做一些更复杂的事情,比如,从用户页面上看到的 URL 中获取其中的一部分,或者匹配任何没有找到页面的 URL。

我将详细解释这个 exec 方法 … 正如我前面说的,URL 模式既有可能是一个字符串,也有可能是一个正则表达式,因此,我首先来检查它是否是一个字符串。如果模式与给定的路径名相同,它返回运行处理程序。如果是一个正则表达式,我们与给定的路径名进行匹配。如果匹配成功,它将获取的参数传递给处理程序,并返回运行这个处理程序。

工作示例

那个例子正好记录到了控制台。我们尝试将它整合到一个页面,看看它是什么样的。

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Router Demo</title>
 <link rel="shortcut icon" href="data:,">
 <script src="/main.js" type="module"></script>
</head>
<body>
 <header>
 <a href="/">Home</a>
 <a href="/users/john_doe">Profile</a>
 </header>
 <main></main>
</body>
</html>

这是 index.html。对于单页面应用程序来说,你必须在服务器侧做一个特别的工作,因为所有未知的路径都将返回这个 index.html。在开发时,我们使用了一个 npm 工具调用了 serve。这个工具去提供静态内容。使用标志 -s/--single,你可以提供单页面应用程序。

使用 Node.js 和安装的 npm(它与 Node 一起安装),运行:

npm i -g serve
serve -s

那个 HTML 文件将脚本 main.js 加载为一个模块。在我们渲染的相关页面中,它有一个简单的 <header> 和一个 <main> 元素。

main.js 文件中:

const main = document.querySelector('main')
const result = router.exec(location.pathname)
main.innerHTML = result

我们调用传递了当前路径名为参数的 router.exec(),然后将 result 设置为 main 元素的 HTML。

如果你访问 localhost 并运行它,你将看到它能够正常工作,但不是预期中的来自一个单页面应用程序。当你点击链接时,单页面应用程序将不会被刷新。

我们将在每个点击的链接的锚点上附加事件监听器,防止出现缺省行为,并做出正确的渲染。因为一个单页面应用程序是一个动态的东西,你预期要创建的锚点链接是动态的,因此要添加事件监听器,我使用的是一个叫 事件委托 的方法。

我给整个文档附加一个点击事件监听器,然后去检查在锚点上(或内部)是否有点击事件。

Router 类中,我有一个注册回调的方法,在我们每次点击一个链接或者一个 popstate 事件发生时,这个方法将被运行。每次你使用浏览器的返回或者前进按钮时,popstate 事件将被发送。

为了方便其见,我们给回调传递与 router.exec(location.pathname) 相同的参数。

class Router {
    // ...
    install(callback) {
        const execCallback = () => {
            callback(this.exec(location.pathname))
        }

        document.addEventListener('click', ev => {
            if (ev.defaultPrevented
                || ev.button !== 0
                || ev.ctrlKey
                || ev.shiftKey
                || ev.altKey
                || ev.metaKey) {
                return
            }

            const a = ev.target.closest('a')

            if (a === null
                || (a.target !== '' && a.target !== '_self')
                || a.hostname !== location.hostname) {
                return
            }

            ev.preventDefault()

            if (a.href !== location.href) {
                history.pushState(history.state, document.title, a.href)
                execCallback()
            }
        })

        addEventListener('popstate', execCallback)
        execCallback()
    }
}

对于链接的点击事件,除调用了回调之外,我们还使用 history.pushState() 去更新 URL。

我们将前面的 main 元素中的渲染移动到 install 回调中。

router.install(result => {
 main.innerHTML = result
})

DOM

你传递给路由器的这些处理程序并不需要返回一个字符串。如果你需要更多的东西,你可以返回实际的 DOM。如:

const homeTmpl = document.createElement('template')
homeTmpl.innerHTML = `
 <div class="container">
 <h1>Home Page</h1>
 </div>
`

function homePage() {
 const page = homeTmpl.content.cloneNode(true)
 // You can do `page.querySelector()` here...
 return page
}

现在,在 install 回调中,你可以去检查 result 是一个 string 还是一个 Node

router.install(result => {
    if (typeof result === 'string') {
        main.innerHTML = result
    } else if (result instanceof Node) {
        main.innerHTML = ''
        main.appendChild(result)
    }
})

这些就是基本的功能。我希望将它共享出来,因为我将在下篇文章中使用到这个路由器。

我已经以一个 npm 包 的形式将它发布了。


via: https://nicolasparada.netlify.com/posts/js-router/

作者:Nicolás Parada 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

只用了一年, Pipenv 就变成了管理软件包依赖关系的 Python 官方推荐资源。

Pipenv 是由 Kenneth Reitz 在一年多前创建的“面向开发者而生的 Python 开发工作流”,它已经成为管理软件包依赖关系的 Python 官方推荐资源。但是对于它解决了什么问题,以及它如何比使用 piprequirements.txt 文件的标准工作流更有用处,这两点仍然存在困惑。在本月的 Python 专栏中,我们将填补这些空白。

Python 包安装简史

为了理解 Pipenv 所解决的问题,看一看 Python 包管理如何发展十分有用的。

让我们回到第一个 Python 版本,这时我们有了 Python,但是没有干净的方法来安装软件包。

然后有了 Easy Install,这是一个可以相对容易地安装其他 Python 包的软件包,但它也带来了一个问题:卸载不需要的包并不容易。

pip 登场,绝大多数 Python 用户都熟悉它。pip 可以让我们安装和卸载包。我们可以指定版本,运行 pip freeze > requirements.txt 来输出一个已安装包列表到一个文本文件,还可以用相同的文本文件配合 pip install -r requirements.txt 来安装一个应用程序需要的所有包。

但是 pip 并没有包含将软件包彼此隔离的方法。我们可能会开发使用相同库的不同版本的应用程序,因此我们需要一种方法来实现这一点。随之而来的是虚拟环境,它使我们能够为我们开发的每个应用程序创建一个小型的、隔离的环境。我们已经看到了许多管理虚拟环境的工具:virtualenvvenvvirtualenvwrapperpyenvpyenv-virtualenvpyenv-virtualenvwrapper 等等。它们都可以很好地使用 piprequirements.txt 文件。

新方法:Pipenv

Pipenv 旨在解决几个问题:

首先,需要 pip 库来安装包,外加一个用于创建虚拟环境的库,以及用于管理虚拟环境的库,再有与这些库相关的所有命令。这些都需要管理。Pipenv 附带包管理和虚拟环境支持,因此你可以使用一个工具来安装、卸载、跟踪和记录依赖性,并创建、使用和组织你的虚拟环境。当你使用它启动一个项目时,如果你还没有使用虚拟环境的话,Pipenv 将自动为该项目创建一个虚拟环境。

Pipenv 通过放弃 requirements.txt 规范转而将其移动到一个名为 Pipfile 的新文档中来完成这种依赖管理。当你使用 Pipenv 安装一个库时,项目的 Pipfile 会自动更新安装细节,包括版本信息,还有可能的 Git 仓库位置、文件路径和其他信息。

其次,Pipenv 希望能更容易地管理复杂的相互依赖关系。你的应用程序可能依赖于某个特定版本的库,而那个库可能依赖于另一个特定版本的库,这些依赖关系如海龟般堆叠起来。当你的应用程序使用的两个库有冲突的依赖关系时,你的情况会变得很艰难。Pipenv 希望通过在一个名为 Pipfile.lock 的文件中跟踪应用程序相互依赖关系树来减轻这种痛苦。Pipfile.lock 还会验证生产中是否使用了正确版本的依赖关系。

另外,当多个开发人员在开发一个项目时,Pipenv 很方便。通过 pip 工作流,凯西可能会安装一个库,并花两天时间使用该库实现一个新功能。当凯西提交更改时,他可能会忘记运行 pip freeze 来更新 requirements.txt 文件。第二天,杰米拉取凯西的改变,测试就突然失败了。这样会花费好一会儿才能意识到问题是在 requirements.txt 文件中缺少相关库,而杰米尚未在虚拟环境中安装这些文件。

因为 Pipenv 会在安装时自动记录依赖性,如果杰米和凯西使用了 Pipenv,Pipfile 会自动更新并包含在凯西的提交中。这样杰米和凯西就可以节省时间并更快地运送他们的产品。

最后,将 Pipenv 推荐给在你项目上工作的其他人,因为它使用标准化的方式来安装项目依赖项和开发和测试的需求。使用 pip 工作流和 requirements.txt 文件意味着你可能只有一个 requirements.txt 文件,或针对不同环境的多个 requirements.txt 文件。例如,你的同事可能不清楚他们是否应该在他们的笔记本电脑上运行项目时是运行 dev.txt 还是 local.txt。当两个相似的 requirements.txt 文件彼此不同步时它也会造成混淆:local.txt 是否过时了,还是真的应该与 dev.txt 不同?多个 requirements.txt 文件需要更多的上下文和文档,以使其他人能够按照预期正确安装依赖关系。这个工作流程有可能会混淆同时并增加你的维护负担。

使用 Pipenv,它会生成 Pipfile,通过为你管理对不同环境的依赖关系,可以避免这些问题。该命令将安装主项目依赖项:

pipenv install

添加 --dev 标志将安装开发/测试的 requirements.txt

pipenv install --dev

使用 Pipenv 还有其他好处:它具有更好的安全特性,以易于理解的格式绘制你的依赖关系,无缝处理 .env 文件,并且可以在一个文件中自动处理开发与生产环境的不同依赖关系。你可以在文档中阅读更多内容。

使用 Pipenv

使用 Pipenv 的基础知识在官方 Python 包管理教程管理应用程序依赖关系部分中详细介绍。要安装 Pipenv,使用 pip

pip install pipenv

要安装在项目中使用的包,请更改为项目的目录。然后安装一个包(我们将使用 Django 作为例子),运行:

pipenv install django

你会看到一些输出,表明 Pipenv 正在为你的项目创建一个 Pipfile

如果你还没有使用虚拟环境,你还会看到 Pipenv 的一些输出,说明它正在为你创建一个虚拟环境。

然后,你将看到你在安装包时常看到的输出。

为了生成 Pipfile.lock 文件,运行:

pipenv lock

你也可以使用 Pipenv 运行 Python 脚本。运行名为 hello.py 的上层 Python 脚本:

pipenv run python hello.py

你将在控制台中看到预期结果。

启动一个 shell,运行:

pipenv shell

如果你想将当前使用 requirements.txt 文件的项目转换为使用 Pipenv,请安装 Pipenv 并运行:

pipenv install requirements.txt

这将创建一个 Pipfile 并安装指定的 requirements.txt。考虑一下升级你的项目!

了解更多

查看 Pipenv 文档,特别是 Pipenv 的基本用法,以帮助你进一步学习。Pipenv 的创建者 Kenneth Reitz 为 Pipenv 在最近的 PyTennessee 发表了一篇演讲:“Python 依赖管理的未来”。这次演讲没有被记录下来,但他的幻灯片有助于理解 Pipenv 所做的以及解决的问题。


via: https://opensource.com/article/18/2/why-python-devs-should-use-pipenv

作者:Lacey Williams Henschel, Jeff Triplett 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

简介:这是一个小脚本,可以在 Linux 桌面上定期自动更改壁纸。

顾名思义,LittleSimpleWallpaperChanger (LSWC)是一个小脚本,可以定期地随机更改壁纸。

我知道在“外观”或“更改桌面背景”设置中有一个随机壁纸选项。但那是随机更改预置的壁纸而不是你添加的壁纸。

因此,在本文中,我们将看到如何使用 LittleSimpleWallpaperChanger 设置包含照片的随机桌面壁纸。

Little Simple Wallpaper Changer (LSWC)

LittleSimpleWallpaperChanger (LSWC) 是一个非常轻量级的脚本,它在后台运行,从用户指定的文件夹中更改壁纸。壁纸以 1 至 5 分钟的随机间隔变化。该软件设置起来相当简单,设置完后,用户就可以不用再操心了。

Little Simple Wallpaper Changer to change wallpapers in Linux

安装 LSWC

点此链接下载 LSWC。压缩文件的大小约为 15KB。

  • 进入下载位置。
  • 右键单击下载的 .zip 文件,然后选择“在此处解压”。
  • 打开解压后的文件夹,右键单击并选择“在终端中打开”。
  • 在终端中复制粘贴命令 bash ./README_and_install.sh 并按回车键。
  • 然后会弹出一个对话框,要求你选择包含壁纸的文件夹。单击它,然后选择你存放壁纸的文件夹。
  • 就是这样。然后重启计算机。

Little Simple Wallpaper Changer for Linux

使用 LSWC

安装时,LSWC 会要求你选择包含壁纸的文件夹。因此,我建议你在安装 LSWC 之前创建一个文件夹并将你想要的壁纸全部移动到那。或者你可以使用图片文件夹中的“壁纸”文件夹。所有壁纸都必须是 .jpg 格式。

你可以添加更多壁纸或从所选文件夹中删除当前壁纸。要更改壁纸文件夹位置,你可以从以下文件中编辑壁纸的位置。

.config/lswc/homepath.conf

删除 LSWC

打开终端并运行以下命令以停止 LSWC:

pkill lswc

在文件管理器中打开家目录,然后按 Ctrl+H 显示隐藏文件,接着删除以下文件:

  • .local 中的 scripts 文件夹
  • .config 中的 lswc 文件夹
  • .config/autostart 中的 lswc.desktop 文件

这就完成了。创建自己的桌面背景幻灯片。LSWC 非常轻巧,易于使用。安装它然后忘记它。

LSWC 功能不是很丰富,但这是有意的。它做了它打算做的事情,那就是更换壁纸。如果你想要一个自动下载壁纸的工具试试 WallpaperDownloader

请在下面的评论栏分享你对这个漂亮的小软件的想法。别忘了分享这篇文章。干杯。


via: https://itsfoss.com/little-simple-wallpaper-changer/

作者:Aquil Roshan 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

用这些有趣好玩的游戏来测试你的战略能力。

游戏一直是 Linux 的弱点之一。由于 Steam、GOG 和其他将商业游戏引入多种操作系统的努力,这种情况近年来有所改变,但这些游戏通常不是开源的。当然,这些游戏可以在开源操作系统上玩,但对于纯粹开源主义者来说还不够好。

那么,一个只使用开源软件的人,能否找到那些经过足够打磨的游戏,在不损害其开源理念的前提下,提供一种可靠的游戏体验呢?当然可以。虽然开源游戏历来不太可能与一些借由大量预算开发的 AAA 商业游戏相匹敌,但在多种类型的开源游戏中,有很多都很有趣,可以从大多数主要 Linux 发行版的仓库中安装。即使某个特定的游戏没有被打包成特定的发行版本,通常也很容易从项目的网站上下载该游戏以便安装和游戏。

这篇文章着眼于益智游戏。我已经写过街机风格游戏棋牌游戏。 在之后的文章中,我计划涉足赛车,角色扮演、战略和模拟经营游戏。

Atomix

Atomix 是 1990 年在 Amiga、Commodore 64、MS-DOS 和其他平台发布的 Atomix) 益智游戏的开源克隆。Atomix 的目标是通过连接原子来构建原子分子。单个原子可以向上、向下、向左或向右移动,并一直朝这个方向移动,直到原子撞上一个障碍物——水平墙或另一个原子。这意味着需要进行规划,以确定在水平上构建分子的位置,以及移动单个部件的顺序。第一关是一个简单的水分子,它由两个氢原子和一个氧原子组成,但后来的关卡是更复杂的分子。

要安装 Atomix,请运行以下命令:

  • 在 Fedora: dnf install atomix
  • 在 Debian/Ubuntu: apt install atomix

Fish Fillets - Next Generation

Fish Fillets - Next Generation 是游戏 Fish fillet 的 Linux 移植版本,它于 1998 年在 Windows 发布,源代码在 2004 年以 GPL 许可证发布。游戏中,两条鱼试图将物体移出道路来通过不同的关卡。这两条鱼有不同的属性,所以玩家需要为每个任务挑选合适的鱼。较大的鱼可以移动较重的物体,但它更大,这意味着它不适合较小的空隙。较小的鱼可以适应那些较小的间隙,但它不能移动较重的物体。如果一个物体从上面掉下来,两条鱼都会被压死,所以玩家在移动棋子时要小心。

要安装 Fish fillet——Next Generation,请运行以下命令:

  • 在 Fedora:dnf install fillets-ng
  • 在 Debian/Ubuntu: apt install fillets-ng

Frozen Bubble

Frozen Bubble 是一款街机风格的益智游戏,从屏幕底部向屏幕顶部的一堆泡泡射击。如果三个相同颜色的气泡连接在一起,它们就会被从屏幕上移除。任何连接在被移除的气泡下面但没有连接其他任何东西的气泡也会被移除。在拼图模式下,关卡的设计是固定的,玩家只需要在泡泡掉到屏幕底部的线以下前将泡泡从游戏区域中移除。该游戏街机模式和多人模式遵循相同的基本规则,但也有不同,这增加了多样性。Frozen Bubble 是一个标志性的开源游戏,所以如果你以前没有玩过它,玩玩看。

要安装 Frozen Bubble,请运行以下命令:

  • 在 Fedora: dnf install frozen-bubble
  • 在 Debian/Ubuntu: apt install frozen-bubble

Hex-a-hop

Hex-a-hop 是一款基于六角形瓦片的益智游戏,玩家需要将所有的绿色瓦片从水平面上移除。瓦片通过移动被移除。由于瓦片在移动后会消失,所以有必要规划出穿过水平面的最佳路径,以便在不被卡住的情况下移除所有的瓦片。但是,如果玩家使用的是次优路径,会有撤销功能。之后的关卡增加了额外的复杂性,包括需要跨越多次的瓦片和使玩家跳过一定数量的六角弹跳瓦片。

要安装 Hex-a-hop,请运行以下命令:

  • 在 Fedora: dnf install hex-a-hop
  • 在 Debian/Ubuntu: apt install hex-a-hop

Pingus

PingusLemmings 的开源克隆。这不是一个精确的克隆,但游戏非常相似。小动物(Lemmings 里是旅鼠,Pingus 里是企鹅)通过关卡入口进入关卡,开始沿着直线行走。玩家需要使用特殊技能使小动物能够到达关卡的出口而不会被困住或者掉下悬崖。这些技能包括挖掘或建桥。如果有足够数量的小动物进入出口,这个关卡将成功完成,玩家可以进入下一个关卡。Pingus 为标准的 Lemmings 添加了一些额外的特性,包括一个世界地图和一些在原版游戏中没有的技能,但经典 Lemmings 游戏的粉丝们在这个开源版本中仍会感到自在。

要安装 Pingus,请运行以下命令:

  • 在 Fedora: dnf install pingus
  • 在 Debian/Ubuntu: apt install pingus

我漏掉你最喜欢的开源益智游戏了吗? 请在下面的评论中分享。


via: https://opensource.com/article/18/6/puzzle-games-linux

作者:Joshua Allen Holm 选题:lujun9972 译者:ZenMoore 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

了解 Yocto、Buildroot、 OpenWRT,和改造过的桌面发行版以确定哪种方式最适合你的项目。

Linux 被部署到比 Linus Torvalds 在他的宿舍里开发时所预期的更广泛的设备。令人震惊的支持了各种芯片,使得Linux 可以应用于大大小小的设备上:从 IBM 的巨型机到不如其连接的端口大的微型设备,以及各种大小的设备。它被用于大型企业数据中心、互联网基础设施设备和个人的开发系统。它还为消费类电子产品、移动电话和许多物联网设备提供了动力。

在为桌面和企业级设备构建 Linux 软件时,开发者通常在他们的构建机器上使用桌面发行版,如 Ubuntu 以便尽可能与被部署的机器相似。如 VirtualBoxDocker 这样的工具使得开发、测试和生产环境更好的保持了一致。

什么是嵌入式系统?

维基百科将嵌入式系统定义为:“在更大的机械或电气系统中具有专用功能的计算机系统,往往伴随着实时计算限制。”

我觉得可以很简单地说,嵌入式系统是大多数人不认为是计算机的计算机。它的主要作用是作为某种设备,而不被视为通用计算平台。

嵌入式系统编程的开发环境通常与测试和生产环境大不相同。它们可能会使用不同的芯片架构、软件堆栈甚至操作系统。开发工作流程对于嵌入式开发人员与桌面和 Web 开发人员来说是非常不同的。通常,其构建后的输出将包含目标设备的整个软件映像,包括内核、设备驱动程序、库和应用程序软件(有时也包括引导加载程序)。

在本文中,我将对构建嵌入式 Linux 系统的四种常用方式进行纵览。我将介绍一下每种产品的工作原理,并提供足够的信息来帮助读者确定使用哪种工具进行设计。我不会教你如何使用它们中的任何一个;一旦缩小了选择范围,就有大量深入的在线学习资源。没有任何选择适用于所有情况,我希望提供足够的细节来指导您的决定。

Yocto

Yocto 项目 定义为“一个开源协作项目,提供模板、工具和方法,帮助您为嵌入式产品创建定制的基于 Linux 的系统,而不管硬件架构如何。”它是用于创建定制的 Linux 运行时映像的配方、配置值和依赖关系的集合,可根据您的特定需求进行定制。

完全公开:我在嵌入式 Linux 中的大部分工作都集中在 Yocto 项目上,而且我对这个系统的认识和偏见可能很明显。

Yocto 使用 Openembedded 作为其构建系统。从技术上讲,这两个是独立的项目;然而,在实践中,用户不需要了解区别,项目名称经常可以互换使用。

Yocto 项目的输出大致由三部分组成:

  • 目标运行时二进制文件:这些包括引导加载程序、内核、内核模块、根文件系统映像。以及将 Linux 部署到目标平台所需的任何其他辅助文件。
  • 包流:这是可以安装在目标上的软件包集合。您可以根据需要选择软件包格式(例如,deb、rpm、ipk)。其中一些可能预先安装在目标运行时二进制文件中,但可以构建用于安装到已部署系统的软件包。
  • 目标 SDK:这些是安装在目标平台上的软件的库和头文件的集合。应用程序开发人员在构建代码时使用它们,以确保它们与适当的库链接

优点

Yocto 项目在行业中得到广泛应用,并得到许多有影响力的公司的支持。此外,它还拥有一个庞大且充满活力的开发人员社区生态系统。开源爱好者和企业赞助商的结合的方式有助于推动 Yocto 项目。

获得 Yocto 的支持有很多选择。如果您想自己动手,有书籍和其他培训材料。如果您想获得专业知识,有许多有 Yocto 经验的工程师。而且许多商业组织可以为您的设计提供基于 Yocto 的 Turnkey 产品或基于服务的实施和定制。

Yocto 项目很容易通过 进行扩展,层可以独立发布以添加额外的功能,或针对项目发布时尚不可用的平台,或用于保存系统特有定制功能。层可以添加到你的配置中,以添加未特别包含在市面上版本中的独特功能;例如,“meta-browser” 层包含 Web 浏览器的清单,可以轻松为您的系统进行构建。因为它们是独立维护的,所以层可以按不同的时间发布(根据层的开发速度),而不是跟着标准的 Yocto 版本发布。

Yocto 可以说是本文讨论的任何方式中最广泛的设备支持。由于许多半导体和电路板制造商的支持,Yocto 很可能能够支持您选择的任何目标平台。主版本 Yocto 分支仅支持少数几块主板(以便达成合理的测试和发布周期),但是,标准工作模式是使用外部主板支持层。

最后,Yocto 非常灵活和可定制。您的特定应用程序的自定义可以存储在一个层进行封装和隔离,通常将要素层特有的自定义项存储为层本身的一部分,这可以将相同的设置同时应用于多个系统配置。Yocto 还提供了一个定义良好的层优先和覆盖功能。这使您可以定义层应用和搜索元数据的顺序。它还使您可以覆盖具有更高优先级的层的设置;例如,现有清单的许多自定义功能都将保留。

缺点

Yocto 项目最大的缺点是学习曲线陡峭。学习该系统并真正理解系统需要花费大量的时间和精力。 根据您的需求,这可能对您的应用程序不重要的技术和能力投入太大。 在这种情况下,与一家商业供应商合作可能是一个不错的选择。

Yocto 项目的开发时间和资源相当高。 需要构建的包(包括工具链,内核和所有目标运行时组件)的数量相当不少。 Yocto 开发人员的开发工作站往往是大型系统。 不建议使用小型笔记本电脑。 这可以通过使用许多提供商提供的基于云的构建服务器来缓解。 另外,Yocto 有一个内置的缓存机制,当它确定用于构建特定包的参数没有改变时,它允许它重新使用先前构建的组件。

建议

为您的下一个嵌入式 Linux 设计使用 Yocto 项目是一个强有力的选择。 在这里介绍的选项中,无论您的目标用例如何,它都是最广泛适用的。 广泛的行业支持,积极的社区和广泛的平台支持使其成为必须设计师的不错选择。

Buildroot

Buildroot 项目定义为“通过交叉编译生成嵌入式 Linux 系统的简单、高效且易于使用的工具。”它与 Yocto 项目具有许多相同的目标,但它注重简单性和简约性。一般来说,Buildroot 会禁用所有软件包的所有可选编译时设置(有一些值得注意的例外),从而生成尽可能小的系统。系统设计人员需要启用适用于给定设备的设置。

Buildroot 从源代码构建所有组件,但不支持按目标包管理。因此,它有时称为固件生成器,因为镜像在构建时大部分是固定的。应用程序可以更新目标文件系统,但是没有机制将新软件包安装到正在运行的系统中。

Buildroot 输出主要由三部分组成:

  • 将 Linux 部署到目标平台所需的根文件系统映像和任何其他辅助文件
  • 适用于目标硬件的内核,引导加载程序和内核模块
  • 用于构建所有目标二进制文件的工具链。

优点

Buildroot 对简单性的关注意味着,一般来说,它比 Yocto 更容易学习。核心构建系统用 Make 编写,并且足够短以便开发人员了解整个系统,同时可扩展到足以满足嵌入式 Linux 开发人员的需求。 Buildroot 核心通常只处理常见用例,但它可以通过脚本进行扩展。

Buildroot 系统使用普通的 Makefile 和 Kconfig 语言来进行配置。 Kconfig 由 Linux 内核社区开发,广泛用于开源项目,使得许多开发人员都熟悉它。

由于禁用所有可选的构建时设置的设计目标,Buildroot 通常会使用开箱即用的配置生成尽可能最小的镜像。一般来说,构建时间和构建主机资源的规模将比 Yocto 项目的规模更小。

缺点

关注简单性和最小化启用的构建方式意味着您可能需要执行大量的自定义来为应用程序配置 Buildroot 构建。此外,所有配置选项都存储在单个文件中,这意味着如果您有多个硬件平台,则需要为每个平台进行每个定制更改。

对系统配置文件的任何更改都需要全部重新构建所有软件包。与 Yocto 相比,这个问题通过最小的镜像大小和构建时间得到了一定的解决,但在你调整配置时可能会导致构建时间过长。

中间软件包状态缓存默认情况下未启用,并且不像 Yocto 实施那么彻底。这意味着,虽然第一次构建可能比等效的 Yocto 构建短,但后续构建可能需要重建许多组件。

建议

对于大多数应用程序,使用 Buildroot 进行下一个嵌入式 Linux 设计是一个不错的选择。如果您的设计需要多种硬件类型或其他差异,但由于同步多个配置的复杂性,您可能需要重新考虑,但对于由单一设置组成的系统,Buildroot 可能适合您。

OpenWRT/LEDE

OpenWRT 项目开始为消费类路由器开发定制固件。您当地零售商提供的许多低成本路由器都可以运行 Linux 系统,但可能无法开箱即用。这些路由器的制造商可能无法提供频繁的更新来解决新的威胁,即使他们这样做,安装更新镜像的机制也很困难且容易出错。 OpenWRT 项目为许多已被其制造商放弃的设备生成更新的固件镜像,让这些设备焕发新生。

OpenWRT 项目的主要交付物是可用于大量商业设备的二进制镜像。它有网络可访问的软件包存储库,允许设备最终用户将新软件添加到他们的系统中。 OpenWRT 构建系统是一个通用构建系统,它允许开发人员创建自定义版本以满足他们自己的需求并添加新软件包,但其主要重点是目标二进制文件。

优点

如果您正在为商业设备寻找替代固件,则 OpenWRT 应位于您的选项列表中。它的维护良好,可以保护您免受制造商固件无法解决的问题。您也可以添加额外的功能,使您的设备更有用。

如果您的嵌入式设计专注于网络,则 OpenWRT 是一个不错的选择。网络应用程序是 OpenWRT 的主要用例,您可能会发现许多可用的软件包。

缺点

OpenWRT 对您的设计限制很多(与 Yocto 和 Buildroot 相比)。如果这些决定不符合您的设计目标,则可能需要进行大量的修改。

在部署的设备中允许基于软件包的更新是很难管理的。按照其定义,这会导致与您的 QA 团队测试的软件负载不同。此外,很难保证大多数软件包管理器的原子安装,以及错误的电源循环可能会使您的设备处于不可预知的状态。

建议

OpenWRT 是爱好者项目或商用硬件再利用的不错选择。它也是网络应用程序的不错选择。如果您需要从默认设置进行大量定制,您可能更喜欢 Buildroot 或 Yocto。

桌面发行版

设计嵌入式 Linux 系统的一种常见方法是从桌面发行版开始,例如 DebianRed Hat,并删除不需要的组件,直到安装的镜像符合目标设备的占用空间。这是 Raspberry Pi 平台流行的 Raspbian发行版的方法。

优点

这种方法的主要优点是熟悉。通常,嵌入式 Linux 开发人员也是桌面 Linux 用户,并且精通他们的选择发行版。在目标上使用类似的环境可能会让开发人员更快地入门。根据所选的分布,可以使用 apt 和 yum 等标准封装工具安装许多其他工具。

可以将显示器和键盘连接到目标设备,并直接在那里进行所有的开发。对于不熟悉嵌入式空间的开发人员来说,这可能是一个更为熟悉的环境,无需配置和使用棘手的跨开发平台设置。

大多数桌面发行版可用的软件包数量通常大于前面讨论的嵌入式特定的构建器可用软件包数量。由于较大的用户群和更广泛的用例,您可能能够找到您的应用程序所需的所有运行时包,这些包已经构建并可供使用。

缺点

将目标平台作为您的主要开发环境可能会很慢。运行编译器工具是一项资源密集型操作,根据您构建的代码的多少,这可能会严重妨碍您的性能。

除了一些例外情况,桌面发行版的设计并不适合低资源系统,并且可能难以充分裁剪目标映像。同样,桌面环境中的预设工作流程对于大多数嵌入式设计来说都不理想。以这种方式获得可再现的环境很困难。手动添加和删除软件包很容易出错。这可以使用特定于发行版的工具进行脚本化,例如基于 Debian 系统的 debootstrap。为了进一步提高可再现性,您可以使用配置管理工具,如 CFEngine(我的雇主 Mender.io 完整披露了 这一工具)。但是,您仍然受发行版提供商的支配,他们将更新软件包以满足他们的需求,而不是您的需求。

建议

对于您打算推向市场的产品,请谨慎使用此方法。这对于爱好者应用程序来说是一个很好的模型;但是,对于需要支持的产品,这种方法很可能会遇到麻烦。虽然您可能能够获得更快的起步,但从长远来看,您可能会花费您的时间和精力。

其他考虑

这个讨论集中在构建系统的功能上,但通常有非功能性需求可能会影响您的决定。如果您已经选择了片上系统(SoC)或电路板,则您的选择很可能由供应商决定。如果您的供应商为特定系统提供板级支持包(BSP),使用它通常会节省相当多的时间,但请研究 BSP 的质量以避免在开发周期后期发生问题。

如果您的预算允许,您可能需要考虑为目标操作系统使用商业供应商。有些公司会为这里讨论的许多选项提供经过验证和支持的配置,除非您拥有嵌入式 Linux 构建系统方面的专业知识,否则这是一个不错的选择,可以让您专注于核心能力。

作为替代,您可以考虑为您的开发人员进行商业培训。这可能比商业操作系统供应商便宜,并且可以让你更加自给自足。这是快速找到您选择的构建系统基础知识的学习曲线。

最后,您可能已经有一些开发人员拥有一个或多个系统的经验。如果你的工程师有倾向性,当你做出决定时,肯定值得考虑。

总结

构建嵌入式 Linux 系统有多种选择,每种都有优点和缺点。将这部分设计放在优先位置至关重要,因为在以后的过程中切换系统的成本非常高。除了这些选择之外,还有新的系统在开发中。希望这次讨论能够为评估新的系统(以及这里提到的系统)提供一些背景,并帮助您为下一个项目做出坚实的决定。


via: https://opensource.com/article/18/6/embedded-linux-build-tools

作者:Drew Moseley 选题:lujun9972 译者:LHRChina 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

最近在尝试配置 awesome WM,因此粗略地学习了一下 lua 。 在学习过程中,我完全被 在 lua 中的应用所镇住了。

表在 lua 中真的是无处不在:首先,它可以作为字典和数组来用;此外,它还可以被用于设置闭包环境、模块;甚至可以用来模拟对象和类。

字典

表最基础的作用就是当成字典来用。 它的键可以是除了 nil 之外的任何类型的值。

t={}
t[{}] = "table"                 -- key 可以是表
t[1] = "int"                    -- key 可以是整数
t[1.1] = "double"               -- key 可以是小数
t[function () end] = "function" -- key 可以是函数
t[true] = "Boolean"             -- key 可以是布尔值
t["abc"] = "String"             -- key 可以是字符串
t[io.stdout] = "userdata"       -- key 可以是userdata
t[coroutine.create(function () end)] = "Thread" -- key可以是thread

当把表当成字典来用时,可以使用 pairs 函数来进行遍历。

for k,v in pairs(t) do
  print(k,"->",v)
end

运行结果为:

1   ->  int
1.1 ->  double
thread: 0x220bb08   ->  Thread
table: 0x220b670    ->  table
abc ->  String
file (0x7f34a81ef5c0)   ->  userdata
function: 0x220b340 ->  function
true    ->  Boolean

从结果中你还可以发现,使用 pairs 进行遍历时的顺序是随机的,事实上相同的语句执行多次得到的结果是不一样的。

表 中的键最常见的两种类型就是整数型和字符串类型。 当键为字符串时,表 可以当成结构体来用。同时形如 t["field"] 这种形式的写法可以简写成 t.field 这种形式。

数组

当键为整数时,表 就可以当成数组来用。而且这个数组是一个 索引从 1 开始 、没有固定长度、可以根据需要自动增长的数组。

a = {}
for i=0,5 do                    -- 注意,这里故意写成了i从0开始
  a[i] = 0
end

当将表当成数组来用时,可以通过长度操作符 # 来获取数组的长度:

print(#a)

结果为:

5

你会发现, lua 认为数组 a 中只有 5 个元素,到底是哪 5 个元素呢?我们可以使用使用 ipairs 对数组进行遍历:

for i,v in ipairs(a) do
  print(i,v)
end

结果为:

1   0
2   0
3   0
4   0
5   0

从结果中你会发现 a 的 0 号索引并不认为是数组中的一个元素,从而也验证了 lua 中的数组是从 1 开始索引的

另外,将表当成数组来用时,一定要注意索引不连贯的情况,这种情况下 # 计算长度时会变得很诡异。

a = {}
for i=1,5 do
  a[i] = 0
end
a[8] = 0                        -- 虽然索引不连贯,但长度是以最大索引为准
print(#a)
a[100] = 0                      -- 索引不连贯,而且长度不再以最大索引为准了
print(#a)

结果为:

8
8

而使用 ipairs 对数组进行遍历时,只会从 1 遍历到索引中断处。

for i,v in ipairs(a) do
  print(i,v)
end

结果为:

1   0
2   0
3   0
4   0
5   0

环境(命名空间)

lua 将所有的全局变量/局部变量保存在一个常规表中,这个表一般被称为全局或者某个函数(闭包)的环境。

为了方便,lua 在创建最初的全局环境时,使用全局变量 _G 来引用这个全局环境。因此,在未手工设置环境的情况下,可以使用 -G[varname] 来存取全局变量的值。

for k,v in pairs(_G) do
  print(k,"->",v)
end
rawequal    ->  function: 0x41c2a0
require ->  function: 0x1ea4e70
_VERSION    ->  Lua 5.3
debug   ->  table: 0x1ea8ad0
string  ->  table: 0x1ea74b0
xpcall  ->  function: 0x41c720
select  ->  function: 0x41bea0
package ->  table: 0x1ea4820
assert  ->  function: 0x41cc50
pcall   ->  function: 0x41cd10
next    ->  function: 0x41c450
tostring    ->  function: 0x41be70
_G  ->  table: 0x1ea2b80
coroutine   ->  table: 0x1ea4ee0
unpack  ->  function: 0x424fa0
loadstring  ->  function: 0x41ca00
setmetatable    ->  function: 0x41c7e0
rawlen  ->  function: 0x41c250
bit32   ->  table: 0x1ea8fc0
utf8    ->  table: 0x1ea8650
math    ->  table: 0x1ea7770
collectgarbage  ->  function: 0x41c650
rawset  ->  function: 0x41c1b0
os  ->  table: 0x1ea6840
pairs   ->  function: 0x41c950
arg ->  table: 0x1ea9450
table   ->  table: 0x1ea5130
tonumber    ->  function: 0x41bf40
io  ->  table: 0x1ea5430
loadfile    ->  function: 0x41cb10
error   ->  function: 0x41c5c0
load    ->  function: 0x41ca00
print   ->  function: 0x41c2e0
dofile  ->  function: 0x41cbd0
rawget  ->  function: 0x41c200
type    ->  function: 0x41be10
getmetatable    ->  function: 0x41cb80
module  ->  function: 0x1ea4e00
ipairs  ->  function: 0x41c970

从 lua 5.2 开始,可以通过修改 _ENV 这个值(lua 5.1 中的 setfenv 从 5.2 开始被废除)来设置某个函数的环境,从而让这个函数中的执行语句在一个新的环境中查找全局变量的值。

a=1                             -- 全局变量中a=1
local env={a=10,print=_G.print} -- 新环境中a=10,并且确保能访问到全局的print函数
function f1()
  local _ENV=env
  print("in f1:a=",a)
  a=a*10                        -- 修改的是新环境中的a值
end

f1()
print("globally:a=",a)
print("env.a=",env.a)
in f1:a=    10
globally:a= 1
env.a=  100

另外,新创建的闭包都继承了创建它的函数的环境。

模块

lua 中的模块也是通过返回一个表来供模块使用者来使用的。 这个表中包含的是模块中所导出的所有东西,包括函数和常量。

定义模块的一般模板为:

module(模块名, package.seeall)

其中 module(模块名) 的作用类似于:

local modname = 模块名
local M = {}                    -- M即为存放模块所有函数及常数的table
_G[modname] = M
package.loaded[modname] = M
setmetatable(M,{__index=_G})    -- package.seeall可以使全局环境_G对当前环境可见
local _ENV = M                  -- 设置当前的运行环境为 M,这样后续所有代码都不需要限定模块名了,所定义的所有函数自动变成M的成员
<函数定义以及常量定义>

return M                        -- module函数会帮你返回module table,而无需手工返回

对象

lua 中之所以可以把表当成对象来用是因为:

  1. 函数在 lua 中是一类值,你可以直接存取表中的函数值。 这使得一个表既可以有自己的状态,也可以有自己的行为:
Account = {balance = 0}
function Account.withdraw(v)
  Account.balance = Account.balance - v
end
  1. lua 支持闭包,这个特性可以用来模拟对象的私有成员变量:
function new_account(b)
  local balance = b
  return {withdraw = function (v) balance = balance -v end,
          get_balance = function () return balance end
  }
end

a1 = new_account(1000)
a1.withdraw(10)
print(a1.get_balance())
990

不过,上面第一种定义对象的方法有一个缺陷,那就是方法与 Account 这个名称绑定死了。 也就是说,这个对象的名称必须为 Accout 否则就会出错。

a = Account
Account = nil
a.withdraw(10)                  -- 会报错,因为Accout.balance不再存在

为了解决这个问题,我们可以给 withdraw 方法多一个参数用于指向对象本身。

Account = {balance=100}
function Account.withdraw(self,v)
  self.balance = self.balance - v
end
a = Account
Account = nil
a.withdraw(a,10)                  -- 没问题,这个时候 self 指向的是a,因此会去寻找 a.balance
print(a.balance)
90

不过由于第一个参数 self 几乎总是指向调用方法的对象本身,因此 lua 提供了一种语法糖形式 object:method(...) 用于隐藏 self 参数的定义及传递。这里冒号的作用有两个,其在定义函数时往函数中地一个参数的位置添加一个额外的隐藏参数 sef, 而在调用时传递一个额外的隐藏参数 self 到地一个参数位置。 即 function object:method(v) end 等价于 function object.method(self,v) end, object:method(v) 等价于 object.method(object,v)

当涉及到类和继承时,就要用到元表和元方法了。事实上,对于 lua 来说,对象和类并不存在一个严格的划分。

当一个对象被另一个表的 __index 元方法所引用时,表就能引用该对象中所定义的方法,因此也就可以理解为对象变成了表的类。

类定义的一般模板为:

function 类名:new(o)
  o = o or {}
  setmetatable(o,{__index = self})
  return o
end

或者:

function 类名:new(o)
  o = o or {}
  setmetatable(o,self)
  self.__index = self
  return o
end

相比之下,第二种写法可以多省略一个表。

另外有一点我觉得有必要说明的就是 lua 中的元方法是在元表中定义的,而不是对象本身定义的,这一点跟其他面向对象的语言比较不同。