snowpack初探

探究snowpack工具的原理以及使用方法

使用

1
2
npm install snowpack -D
npx create-snowpack-app new-dir --template @snowpack/app-template-vue
  • 使用create-snowpack-app可以快速的创建一个初始应用,并且配置好了基础设置

install命令

  • 把所有对node_modules的依赖,找到支持module的包,复制对应的文件到web_modules目录下,引用改包的地方就会被修改成

    1
    2
    3
    4
    5
    6
    // Your Code:
    import * as React from "react";
    import * as ReactDOM from "react-dom";
    // Build Output:
    import * as React from "/web_modules/react.js";
    import * as ReactDOM from "/web_modules/react-dom.js";
  • 怎么判断一个包是否支持es module,是从package.json中找,查找顺序是

    1
    depManifest['browser:module'] || depManifest.module || depManifest['main:esnext'] || depManifest.browser
  • 所有文件会过一遍rollup打包,最终的文件是es module模式的,rollup打包时,使用了以下插件

    1
    2
    3
    4
    5
    import rollupPluginAlias from '@rollup/plugin-alias';
    import rollupPluginCommonjs from '@rollup/plugin-commonjs';
    import rollupPluginJson from '@rollup/plugin-json';
    import rollupPluginNodeResolve from '@rollup/plugin-node-resolve';
    import rollupPluginReplace from '@rollup/plugin-replace';
    • plugin-alias的作用是用来支撑config中的alias配置
    • plugin-commons,把CommonJS格式的包转成es module,rollup才能识别处理
    • plugin-josn,把json文件转成es module
    • plugin-node-resolve。应用node查找包的算法,主要是处理引用node_module中的包的情况
    • plugin-replace的作用是替换例如process.env.NODE_ENV等变量

build命令

  • 会先执行install
  • 会默认添加两个scripts,’mount:web_modules’(把.cache中的build中的内容mount到最终的build目录), ‘build: js,jsx,ts,tsx’,
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    id: 'mount:web_modules',
    type: 'mount',
    match: [ 'web_modules' ],
    cmd: 'mount $WEB_MODULES --to /web_modules',
    args: {
    fromDisk: '/Users/xx//node_modules/.cache/snowpack/build',
    toUrl: '/web_modules'
    }
    },
    {
    id: 'build:js,jsx,ts,tsx',
    type: 'build',
    match: [ 'js', 'jsx', 'ts', 'tsx' ],
    cmd: '(default) esbuild',
    plugin: { build: [AsyncFunction: build] }
    }

dev命令

  • 会判断是否需要install,如果需要会执行
  • createServer,在server的相应端,会执行各种逻辑,返回各种静态资源,会优先返回cache中的资源

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    const server = createServer(async (req, res) => {
    ...
    const [fileLoc, selectedWorker] = await getFileFromUrl(reqPath);
    // 1. Check the hot build cache. If it's already found, then just serve it.
    let hotCachedResponse: string | Buffer | undefined = inMemoryBuildCache.get(fileLoc);
    if (!hotCachedResponse) {
    // 2. Load the file from disk. We'll need it to check the cold cache or build from scratch.
    fileContents = await fs.readFile(fileLoc, getEncodingType(requestedFileExt));
    // 3. Check the persistent cache. If found, serve it via a "trust-but-verify" strategy.
    // Build it after sending, and if it no longer matches then assume the entire cache is suspect.
    // In that case, clear the persistent cache and then force a live-reload of the page.
    if (cache) {
    sendFile()
    const checkFinalBuildAnyway = await buildFile(
    fileContents,
    fileLoc,
    reqPath,
    fileBuilder,
    );
    return
    }
    // 4. Final option: build the file, serve it, and cache it.
    finalBuild = await buildFile(fileContents, fileLoc, reqPath, fileBuilder);
    const wrappedResponse = await wrapResponse(finalBuild.result, finalBuild.resources?.css);
    sendFile(req, res, wrappedResponse, responseFileExt);
    }
  • 所以dev命令执行的时候很快,server会立马起来,打开浏览器,等浏览器访问对应资源的时候,再进行build。

HMR

  • chokidar watch所有文件,add,change,unlink事件就会触发事件,通过WebSocket传递给client

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const watcher = chokidar.watch(
    mountedDirectories.map(([dirDisk]) => dirDisk),
    {
    ignored: config.exclude,
    persistent: true,
    ignoreInitial: true,
    disableGlobbing: false,
    },
    );
    watcher.on('add', (fileLoc) => onWatchEvent(fileLoc));
    watcher.on('change', (fileLoc) => onWatchEvent(fileLoc));
    watcher.on('unlink', (fileLoc) => onWatchEvent(fileLoc));
  • 需要自己手动加入如下代码

    1
    2
    3
    4
    5
    6
    if (import.meta.hot) {
    import.meta.hot.accept();
    import.meta.hot.dispose(() => {
    app.unmount();
    });
    }
  • 针对change事件,不刷新页面实现更新。其原理是client端得到消息需要更新时,会重新import需要更新的资源,并且带上时间戳。server端就会把里面的依赖import资源都带上时间戳,从而实现刷新。

css的打包

直接引入css

  • 对于css文件,使用proxy的模式加载,如在源代码中这么使用

    1
    import './a.css'
  • 最终a.css会被buidl为a.css.proxy.js,其内容是

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    const code = "div {\n background-color: red;\n}";

    const styleEl = document.createElement("style");
    const codeEl = document.createTextNode(code);
    styleEl.type = 'text/css';

    styleEl.appendChild(codeEl);
    document.head.appendChild(styleEl);

    // 下面的代码只有在开启了hmr时才会注入
    import * as __SNOWPACK_HMR_API__ from '/__snowpack__/hmr.js';
    import.meta.hot = __SNOWPACK_HMR_API__.createHotContext(import.meta.url);
    import.meta.hot.accept();
    import.meta.hot.dispose(() => {
    document.head.removeChild(styleEl);
    });

使用css module

  • 必须以.module.css结尾,会被css-modules-loader-core这个包处理一遍
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import Core from 'css-modules-loader-core';
    let core = new Core();
    const {injectableSource, exportTokens} = await core.load(code, url, () => {
    throw new Error('Imports in CSS Modules are not yet supported.');
    });
    return `
    export let code = ${JSON.stringify(injectableSource)};
    let json = ${JSON.stringify(exportTokens)};
    export default json;
    `

支持jsx,ts

  • 使用esbuild这个包实现对这俩语法的支持,默认支队.ts,.jsx后缀的文件进行处理

esm的支持情况

  • 除了ie,如果对兼容性要求不要的场景,在pc端可以开始尝试使用了
    mdn
    cani

参考文档

https://zhuanlan.zhihu.com/p/144993158
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import
https://www.caniuse.com/#search=import