告別webpack,直接運行npm包

爲何要打包?

2019年,距離es6正式發佈已通過去了4年多了,es6給咱們帶來了許多新特性,包括全新的JavaScript模塊系統(ESM),它能夠直接在瀏覽器運行。但通常咱們開發項目,仍是要引入Browserify和Webpack等打包工具進行打包,誠然,這些打包工具能夠給項目帶來不少好處、好比混淆、壓縮和轉譯代碼等等。但與此同時,也帶給項目極大的複雜性,各類各樣的配置文件和插件等。不少時候,咱們不得不進行打包,由於npm的存在,咱們一般會引用不少註冊在npm上面的包,早期的npm包大部分都是common.js(cjs)風格的,這意味着瀏覽器不能直接運行,因此通常都要通過打包工具轉換成瀏覽器支持的模塊。javascript

現狀

npm後面推出了module入口的模塊,即ESM風格的模塊,到目前爲止,已經超過70000個包提供了ESM的版本,已經基本知足咱們開發的須要了。因此爲何咱們不直接跳過打包這個步驟,直接運行呢?事實上,本身寫的ESM模塊能夠直接運行,但大部分npm包都會有依賴,瀏覽器沒辦法從node_modules導入這些依賴,因此咱們依然須要對代碼進行打包,轉譯。html

@pika/web

基於上述緣由,@pika/web誕生了。@pika/web不是構建工具,而是依賴安裝工具,它能夠幫助你構建npm包,而後直接在瀏覽器上運行,只須要一行代碼:java

npx @pika/web
複製代碼

使用方法

@pika/web使用起來十分簡單,咱們首先須要在package.jsondependencies顯式聲明項目所依賴的包:node

// package.json
// ...
"dependencies": {
  "test": "^1.0.0"
},
複製代碼

而後在項目中引用這個包:git

// index.js
import { name } from 'test'
console.log(name)
複製代碼

而後執行npx @pika/webes6

pika會在項目根目錄生成一個 web_modules的文件夾,裏面放着項目依賴的文件,最後一步就是將替換項目引用依賴的地址

// before
import { name } from 'test'
// after
import { name } from './web_modules/test.js'
複製代碼

最後一步,由於咱們的代碼須要在瀏覽器執行,因此須要嵌入到html文件中:github

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
</body>
<script type="module" src="./index.js"></script>
</html>
複製代碼

而後用瀏覽器打開文件,你會獲得你想要的結果。web

可能有的同窗會有說:「個人項目引用的依賴不少,總不能一個個去替換地址吧,多麻煩啊!」,沒問題,pika提供了一個babel的插件幫助咱們替換模塊引用。npm

源碼解密

@pika/web的原理很簡單,首先它會從package.jsondependencies種尋找項目依賴,收集好依賴就會逐個解析依賴,生成rollup的配置文件,最後用rollup將每個依賴打包成模塊,放到web_modules中。json

  • 收集依賴
// ...
const pkgManifest = require(path.join(cwd, 'package.json'));
  const {namedExports, webDependencies} = pkgManifest['@pika/web'] || {
    namedExports: undefined,
    webDependencies: undefined,
  };
  const doesWhitelistExist = !!webDependencies;
  const arrayOfDeps = webDependencies || Object.keys(pkgManifest.dependencies || {});
// ...
複製代碼
  • 逐個解析
// ...
const depManifestLoc = resolveFrom(cwd, `${dep}/package.json`);
  const depManifest = require(depManifestLoc);
  let foundEntrypoint: string = depManifest.module;
  // If the package was a part of the explicit whitelist, fallback to it's main CJS entrypoint.
  if (!foundEntrypoint && isExplicit) {
    foundEntrypoint = depManifest.main || 'index.js';
  }
// ...
複製代碼
  • 生成rollup打包配置
// ...
const inputOptions = {
      input: depObject,
      plugins: [
        rollupPluginNodeResolve({
          mainFields: ['browser', 'module', !isStrict && 'main'].filter(Boolean),
          modulesOnly: isStrict, // Default: false
          extensions: ['.mjs', '.cjs', '.js', '.json'], // Default: [ '.mjs', '.js', '.json', '.node' ]
          // whether to prefer built-in modules (e.g. `fs`, `path`) or local ones with the same names
          preferBuiltins: false, // Default: true
        }),
        !isStrict &&
          rollupPluginCommonjs({
            extensions: ['.js', '.cjs'], // Default: [ '.js' ]
            namedExports: knownNamedExports,
          }),
        !!isBabel &&
          rollupPluginBabel({
            compact: false,
            babelrc: false,
            presets: [
              [
                babelPresetEnv,
                {
                  modules: false,
                  targets: hasBrowserlistConfig ? undefined : '>0.75%, not ie 11, not op_mini all',
                },
              ],
            ],
          }),
        !!isOptimized && rollupPluginTerser(),
      ],
      }) as any,
    };
    const outputOptions = {
      dir: destLoc,
      format: 'esm' as 'esm',
      sourcemap: sourceMap === undefined ? isOptimized : sourceMap,
      exports: 'named' as 'named',
      chunkFileNames: 'common/[name]-[hash].js',
    };
    const packageBundle = await rollup.rollup(inputOptions);
    await packageBundle.write(outputOptions);
    fs.writeFileSync(
      path.join(destLoc, 'import-map.json'),
      JSON.stringify({imports: importMap}, undefined, 2),
      {encoding: 'utf8'},
    );
// ...
複製代碼
相關文章
相關標籤/搜索