Implementar un paquete de JavaScript simple

prefacio

En el entorno front-end actual, la ingeniería front-end se deriva gradualmente de la anterior html, cssy de lo simple a lo complejo, volviéndose cada vez más complicado. El más complicado nos pertenece . Los ingenieros han aparecido para especializarse en la configuración .jswebpackwebpackwebpack

Hay decenas de miles de herramientas de empaque de ingeniería de front-end, quién es la tuya NO.One.

Este artículo implementa una herramienta de empaquetado simple javaScript, que no implica no javaScriptempaquetado, como: css, html, 静态文件etc.

ambiente

Nuestro ordenador necesita estar equipado con nodeun entorno.

Piezas necesarias Herramientas

fs

fsLos módulos se usan para manipular archivos. Este módulo solo nodese puede usar en el entorno, no en el navegador.

camino

pathUn módulo es un módulo para trabajar con archivos y rutas de archivos.

@babel/parser

@babel/parserEl módulo se utiliza para recibir código fuente, realizar análisis léxico, análisis de sintaxis y generar 抽象语法树(Abstract Syntax Tree), denominado ast.

@babel/atravesar

@babel/traverseLos módulos para actualizaciones transversales que usamos @babel/parsergeneraron AST. Operar en nodos específicos.

@babel/núcleo

@babel/coretransformEl código utilizado para compilar nuestro módulo en el módulo se puede convertir a la primera versión del código para que sea más compatible. En este artículo transformFromAstSync, el efecto es el mismo.

@babel/preset-env

@babel/preset-enves规范Un módulo es un módulo de herramienta preestablecido para un entorno inteligente, que nos permite escribir código utilizando la última versión , sin tener que administrar todos los tediosos detalles de qué transformaciones de sintaxis se requieren para el entorno de destino.

escribir un paquete

Combinaremos los módulos de herramientas anteriores para escribir una jsherramienta de empaquetado propia. Si necesitamos empaquetar elementos que no son de jscontenido, necesitamos otras herramientas de módulo.

Lo que este artículo logra solo se puede empaquetar js, hagámoslo juntos.

Hay un pequeño detalle para recordar a mis amigos

El contenido empaquetado en macel sistema es el mismo que imprime el terminal, pero la descarga está oculta.终端windowmac

Construcción del entorno

Primero necesitamos crear una nueva carpeta, luego ejecutar npm init/ pnpm initpara generar package.jsonarchivos. Luego instale los módulos anteriores.


//npm
npm i @babel/core @babel/parser @babel/preset-env @babel/traverse

//pnpm
pnpm i @babel/core @babel/parser @babel/preset-env @babel/traverse

main.jsarchivo nuevo

Creamos un nuevo main.jsarchivo para escribir nuestro código, por supuesto, puede usar otros nombres de archivo.

nuevo srcdirectorio

Aquí necesitamos crear un nuevo srcdirectorio para contener el código que escribimos.

En srcel directorio, creamos dos nuevos jsarchivos, de la siguiente manera:


// foo.js
const foo = () => {
    
    
    console.log('我是foo');
}

export {
    
    
    foo
}

Vamos a crear un nuevo index.jsarchivo, importarlo foo.jsy index.jsejecutar fooel método en el método. Luego realizamos indexdefensas.

// index.js

import {
    
     foo } from "./foo.js"

const index = () => {
    
    
    foo()
    console.log('我是index');
    for(let item of [1, 2]){
    
    
        console.log(item);
    }
}

index()

escribirmain.js

Ahora es el main.jsmomento de que empecemos a escribir.

Introduce el módulo de la herramienta que solo necesitábamos, aquí necesitamos usar requirela forma de referencia,


const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
// 这里需要添加.default来引入@babel/traverse模块,因为require不支持default的导出,故添加此内容
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

leer el contenido del archivo

Agregamos readFileun método para leer el contenido del archivo que escribimos js, aquí usamos el método fsen el módulo readFileSyncy establecemos el formato del contenido en utf-8.

Luego pasamos index.jsla ruta al archivo y ejecutamos el método.

const readFile = (fileName) => {
    
    
    const content = fs.readFileSync(fileName, 'utf-8')

    console.log(content);
}


readFile('./src/index.js')

Ejecutamos en la terminal node main.js.

Vemos que la terminal imprime index.jsel contenido de nuestro archivo. El contenido es exactamente el mismo que el nuestro index.js, la diferencia es que la cadena impresa es una cadena con un \ncarácter de nueva línea agregado dentro.

import {
    
     foo } from "./foo.js"

const index = () => {
    
    
    foo()
    console.log('我是index');
    for(let item of [1, 2]){
    
    
        console.log(item);
    }
}

index()

Generar astun árbol de sintaxis a partir del contenido del archivo obtenido

Arriba tenemos el código que escribimos, ahora necesitamos @babel/parsergenerar el nuestro a través de la herramienta ast.

Lo agregamos readFileen el método @babel/parsery lo configuramos sourceTypeen module. Y aún ejecutar en la terminal node main.js.

    const ast = parser.parse(content, {
    
    
        sourceType: 'module'
    })

    console.log(ast);

El resultado impreso es el siguiente, que es un nodenodo de formato, y el contenido de nuestro código está en program-> body.

    Node {
    
    
        type: 'File',
        start: 0,
        end: 165,
        loc: SourceLocation {
    
    
            start: Position {
    
     line: 1, column: 0, index: 0 },
            end: Position {
    
     line: 12, column: 7, index: 165 },
            filename: undefined,
            identifierName: undefined
        },
        errors: [],
        program: Node {
    
    
            type: 'Program',
            start: 0,
            end: 165,
            loc: SourceLocation {
    
    
            start: [Position],
            end: [Position],
            filename: undefined,
            identifierName: undefined
            },
            sourceType: 'module',
            interpreter: null,
            body: [ [Node], [Node], [Node] ],
            directives: []
        },
        comments: []
    }

@babel/traverseActualizar nuestro recorrido usandoast

Aquí usamos @babel/traversela herramienta para iterar sobre lo que acabamos de generar ast.

En este entorno, necesitamos crear un nuevo dependenciesobjeto llamado , que se usa para instalar astlas dependencias que hemos procesado.

Simplemente pasaremos yast agregaremos un parámetro formal para recibir la ruta de cada archivo para una de las funciones.optionImportDeclaration

    const dependencies = {
    
    }

    traverse(ast, {
    
    
        ImportDeclaration: ({
     
      node }) => {
    
    
            
        }
    })

Manejamos pathrutas a nuestros archivos a través de módulos.

    const dirName = path.dirname(fileName)

Necesitamos hacer un procesamiento adicional con nuestros nombres de archivo y rutas. Y regex para reemplazar las barras invertidas.

    const dir = './' + path.join(dirName, node.source.value).replace('\\', '/')

En el código anterior node.source.valueestán todos los nombres de archivo y las rutas que obtuvimos astsegún.

Almacenamos la ruta del archivo que obtuvimos dependenciesen el objeto.

    dependencies[node.source.value] = dir

Finalmente ejecutamos en la terminal node main.jse imprimimos nuestro dependenciesobjeto. El contenido impreso es consistente con la ruta del archivo que necesitamos compilar.

    {
    
     './foo.js': './src/foo.js' }

@babel/coreTranscodificar nuestro código usando

Aquí necesitamos usar la solución @babel/coreen la herramienta transformpara traducir nuestro código, para que nuestro código también pueda ejecutarse normalmente en la primera versión del navegador.

Aquí usamos lo último api transformFromAstSyncpara transpilar nuestro código.

transformFromAstSyncQué hace: vuelve a compilar la transcodificación que acabamos de modificar asten nuestro código.

Solo necesitamos su código transformado, no necesitamos nada más, así que desestructuramos su resultado para obtener solo su código.

    const {
    
     code } = babel.transformFromAstSync(ast, null, {
    
    })

Necesitamos usarlo aquí @babel/preset-envpara degradar nuestro código, es decir, se escribe la nueva versión de la especificación que usamos y debemos transferirla de nuevo a la versión anterior de la especificación. Si no lo manejamos aquí, nuestro código no lo manejará y la salida será como es.

Entonces necesitamos agregarle una presetspropiedad y ponerla en nuestra @babel/preset-envherramienta. Aquí modulesconfiguramos la propiedad falsepara permitirle generar esmel código formateado.

Otras extensiones de propiedad: commonjs, amd, umd, systemjs,auto

    const {
    
     code } = babel.transformFromAstSync(ast, null, {
    
    
        presets: [
            [
                "@babel/preset-env",
                {
    
    
                    modules: false
                }
            ]
        ]
    })

Lo ejecutamos en la terminal node main.js, y el contenido impreso es el siguiente:

import {
    
     foo } from "./foo.js";
var index = function index() {
    
    
  foo();
  console.log('我是index');
  for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
    
    
    var item = _arr[_i];
    console.log(item);
  }
};
index();

El código completo del método readFile:

const readFile = (fileName) => {
    
    
    const content = fs.readFileSync(fileName, 'utf-8')

    const ast = parser.parse(content, {
    
    
        sourceType: 'module'
    })

    const dependencies = {
    
    }

    traverse(ast, {
    
    
        ImportDeclaration: ({
     
      node }) => {
    
    
            const dirName = path.dirname(fileName)
            const dir = './' + path.join(dirName, node.source.value).replace('\\', '/')
            dependencies[node.source.value] = dir
        }
    })

    const {
    
     code } = babel.transformFromAstSync(ast, null, {
    
    
        presets: [
            [
                "@babel/preset-env",
                {
    
    
                    modules: false
                }
            ]
        ]
    })

    return {
    
    
        fileName,
        dependencies,
        code
    }

}

Ha degradado con éxito nuestro código y devolvemos nuestro , 文件名/文件路径, 依赖关系(dependencies), para nuestro uso posterior.代码(code)return

Escribir un generador de dependencia

Necesitamos crear un nuevo createDependciesGraphmétodo llamado , que recopilará las dependencias de nuestros archivos. Agregue un parámetro formal para recibir el nombre de archivo que le pasamos.

const createDependciesGraph = entry => {
    
    }

Cree una graphListmatriz nombrada para contener el valor de retorno de nuestro readFilemétodo return.

const graphList = [readFile(entry)]

Necesitamos procesamiento recursivo aquí graphListpara evitar múltiples dependencias en él.

for(let i = 0;i < graphList.length; i++){
    
    }

Necesitamos organizar cada elemento en el bucle, por lo que declaramos que uno itemse mantiene.

const item = graphList[i]

También necesitamos almacenar temporalmente las dependencias de cada elemento aquí.

const {
    
     dependencies } = item

Aquí necesitamos agregar un juicio.Si hay una relación de dependencia, continuamos ciclando su capa de dependencia nuevamente y la insertamos, graphListpara anidar recursivamente el ciclo y leer el contenido del archivo hasta que finalice el ciclo.

    if(dependencies){
    
    
        for(let j in dependencies){
    
    
            graphList.push( readFile( dependencies[j] ) )
        }
    }

Código completo para esta parte:

const createDependciesGraph = entry => {
    
    
    const graphList = [readFile(entry)]

    for(let i = 0;i < graphList.length; i++){
    
    
        const item = graphList[i]
        const {
    
     dependencies } = item

        if(dependencies){
    
    
            for(let j in dependencies){
    
    
                graphList.push(
                    readFile(dependencies[j])
                )
            }
        }
    }
    
    console.log(graphList);
}

Imprimimos lo procesado graphList, terminal input node main.js, el resultado es el siguiente:

[
  {
    
    
    fileName: './src/index.js',
    dependencies: {
    
     './foo.js': './src/foo.js' },
    code: import {
    
     foo } from "./foo.js"; 
         var index = function index() {
    
    
           foo();
           console.log('我是index');
           for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
    
    
             var item = _arr[_i];
             console.log(item);
           }
         };
         index();
  },
  {
    
    
    fileName: './src/foo.js',
    dependencies: {
    
    },
    code: var foo = function foo() {
    
      console.log('我是foo');};export {
    
     foo };
  }
]

Peinado de capa de relaciones

Hemos visto que nuestra capa de relación se ha impreso por completo en este momento, y debemos resolverlo ahora.

Creamos un nuevo objeto para contener nuestra capa de relación peinada.

const graph = {
    
    }

Aquí recorremos graphListla matriz y graphescribimos nuestra capa de dependencia detallada en ella.

    for(let item of graphList){
    
    
        const {
    
    dependencies, code} = item
        graph[item.fileName] = {
    
    
            dependencies,
            code
        }
    }

Imprimamos el contenido ordenado en este momento, todavía escribiendo en la terminal node main.js:

{
    
    
  './src/index.js': {
    
    
    dependencies: {
    
     './foo.js': './src/foo.js' },
    code: import {
    
     foo } from "./foo.js";
      var index = function index() {
    
    
        foo();\n' +
        console.log('我是index');
        for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
    
    
          var item = _arr[_i];
          console.log(item);
        }
      };
      index();
  },
  './src/foo.js': {
    
    
    dependencies: {
    
    },
    code: var foo = function foo() {
    
      console.log('我是foo');};export {
    
     foo };
  }
}

Necesitamos devolver la capa de relación ordenada returnpara nuestro uso posterior.

return graph

El código completo del método createDependenciesGraph:

const createDependciesGraph = entry => {
    
    
    const graphList = [readFile(entry)]

    for(let i = 0;i < graphList.length; i++){
    
    
        const item = graphList[i]
        const {
    
     dependencies } = item

        if(dependencies){
    
    
            for(let j in dependencies){
    
    
                graphList.push(
                    readFile(dependencies[j])
                )
            }
        }
    }

    const graph = {
    
    }
    for(let item of graphList){
    
    
        const {
    
    dependencies, code} = item
        graph[item.fileName] = {
    
    
            dependencies,
            code
        }
    }

    return graph
}

Primero creemos un método de administración de archivos

En este paso, primero creamos un método de administración de carpetas, que se usa para borrar el directorio y volver a crearlo cada vez que empacamos.

Declaramos un rmdirmétodo llamado, que administra nuestra carpeta de directorio de empaque

const rmdir = async () =>  {
    
    }

Démosle returnuna new Promiseinstancia interna, en cuanto a la razón, la usaremos más adelante y la entenderemos, lo cual es conveniente para que la usemos más adelante.

const rmdir = async () =>  {
    
    
    return new Promise(async (resolve, reject) => {
    
    
    
    })
}

Declaramos uno errpara obtener errores cuando operamos carpetas y archivos,

let err = null

Leemos el estado de nuestra carpeta de embalaje actual, la vaciamos y la borramos si existe. recursiveIndica si se debe eliminar la carpeta, trueque es eliminar.

const isDir = fs.existsSync('dist')

if(isDir){
    
    
    fs.rmdir('dist', {
    
    recursive: true},  error => {
    
    
        if(error){
    
    
            err = error
        }
    })
}

Aquí hacemos un juicio equivocado, y cuando erres cierto lanzamos un error y returnsalimos.

if(err){
    
    
    reject(err)
    return
}

Aquí lo usamos setTimeoutpara retrasar la notificación de éxito para evitar eliminar carpetas y crear carpetas al mismo tiempo, lo que resulta en una creación fallida.

setTimeout(()=>{
    
    
    resolve()
}, 1000)

rmdir código completo:

const rmdir = async () =>  {
    
    
    return new Promise(async (resolve, reject) => {
    
    

        let err = null

        const isDir = fs.existsSync('dist')

        if(isDir){
    
    
            fs.rmdir('dist', {
    
    recursive: true},  error => {
    
    
                if(error){
    
    
                    err = error
                }
            })
        }

        if(err){
    
    
            reject(err)
            return
        }
        
        setTimeout(()=>{
    
    
            resolve()
        }, 1000)
    })
}

método generador de código

Aquí estoy usando esbuildun modo de salida empaquetado, es decir, los archivos empaquetados se generan sincrónicamente de acuerdo con las reglas del directorio cuando se creó el proyecto.

Aquí creamos un generateCodemétodo llamado , hacemos nuestra llamada de entrada de generación de código y escribimos el archivo generado.

const generateCode = entry => {
    
    }

Llame al método dentro de él createDependciesGraphy entry(打包的入口文件)páselo. Y declarar codeInfoaceptar.

const codeInfo = createDependciesGraph(entry)

Podemos imprimirlo primero para ver codeInfocómo se ve.

{
    
    
  './src/index.js': {
    
    
    dependencies: {
    
     './foo.js': './src/foo.js' },
    code: import {
    
     foo } from "./foo.js";
      var index = function index() {
    
    
        foo();
        console.log('我是index');
        for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
    
    
          var item = _arr[_i];
          console.log(item);
        }
      };
      index();
  },
  './src/foo.js': {
    
    
    dependencies: {
    
    },
    code: var foo = function foo() {
    
      console.log('我是foo');};export {
    
     foo };
  }
}

Ahora creamos carpetas y escribimos archivos basados ​​en dependencias.

En este momento, hemos alineado rmdirel método justo ahora, llamamos rmdiral método y .thenescribimos nuestro proceso de creación de archivos en él. Esta es la razón por la que rmdirse devolvió uno cuando Promisese creó hace un momento. Después de eliminar y vaciar el directorio del paquete, se crean la carpeta y los archivos del paquete, de modo que evitamos el problema de crear y eliminar carpetas y archivos al mismo tiempo.

rmdir().then(()=>{
    
    })

Ahora vamos a crear la carpeta del directorio de empaquetado.

fs.mkdir('dist', () => {
    
    })

Hacemos un bucle de nuestras dependencias en la devolución de llamada para crear la carpeta empaquetada, porque codeInfoes un objeto, no podemos usarlo for..of...y usamos el es6nuevo en for..in...

for(let key in codeInfo){
    
    }

Aquí creamos una carpeta con el mismo nombre y escribimos el código especificado en un archivo con el mismo nombre. Aquí obtenemos splitel nombre del archivo actual por el método que usamos y tomamos el último elemento, porque el último elemento es nuestro nombre de archivo.

let value = key.split('/')
value = value[value.length - 1]

Creamos un precio de venta basado en el nombre del archivo obtenido anteriormente y escribimos el código correspondiente en el archivo.

let value = key.split('/')
value = value[value.length - 1]

fs.writeFile(`./dist/${
      
      value}`, codeInfo[key]['code'], [], () => {
    
    })

código completo del método generateCode

const generateCode = entry => {
    
    
    const codeInfo = createDependciesGraph(entry)

    console.log(codeInfo);

    rmdir().then(()=>{
    
    
        fs.mkdir('dist', () => {
    
    
            for(let key in codeInfo){
    
    
                let value = key.split('/')
                value = value[value.length - 1]

                fs.writeFile(`./dist/${
      
      value}`, codeInfo[key]['code'], [], () => {
    
    })
            
            }
        })
    })

}

Necesitamos main.jsllamar al método de nuestro generateCodegenerador de código en . Necesitamos pasar el archivo de entrada del archivo del paquete mientras llamamos.

generateCode('./src/index.js')

Terminamos de escribir, ahora vamos a ejecutarlo, ingresarlo en la terminal node main.jsy permitirlo.

Encontraremos que nuestro directorio de proyectos ha generado un directorio que contiene distnuestros archivos.srcjs

imagen.png

Veamos foo.jssi index.jsel archivo contiene srcel contenido del directorio. ,

foo.js

var foo = function foo() {
    
    
  console.log('我是foo');
};
export {
    
     foo };

índice.js

import {
    
     foo } from "./foo.js";
var index = function index() {
    
    
  foo();
  console.log('我是index');
  for (var _i = 0, _arr = [1, 2]; _i < _arr.length; _i++) {
    
    
    var item = _arr[_i];
    console.log(item);
  }
};
index();

Verifique que los archivos empaquetados permitan

Creamos uno nuevo index.html, e importamos los archivos disten el directorio index.js.

<script src="./dist/index.js" type="module"></script>

El efecto es el siguiente:

imagen.png

Nuestros archivos empaquetados se pueden ejecutar normalmente.

código completo



const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')

const readFile = (fileName) => {
    
    
    const content = fs.readFileSync(fileName, 'utf-8')

    const ast = parser.parse(content, {
    
    
        sourceType: 'module'
    })

    const dependencies = {
    
    }

    traverse(ast, {
    
    
        ImportDeclaration: ({
     
      node }) => {
    
    
            const dirName = path.dirname(fileName)
            const dir = './' + path.join(dirName, node.source.value).replace('\\', '/')
            dependencies[node.source.value] = dir
        }
    })

    const {
    
     code } = babel.transformFromAstSync(ast, null, {
    
    
        presets: [
            [
                "@babel/preset-env",
                {
    
    
                    modules: false
                }
            ]
        ]
    })

    return {
    
    
        fileName,
        dependencies,
        code
    }

}

const createDependciesGraph = entry => {
    
    
    const graphList = [readFile(entry)]

    for(let i = 0;i < graphList.length; i++){
    
    
        const item = graphList[i]
        const {
    
     dependencies } = item

        if(dependencies){
    
    
            for(let j in dependencies){
    
    
                graphList.push(
                    readFile(dependencies[j])
                )
            }
        }
    }

    const graph = {
    
    }
    for(let item of graphList){
    
    
        const {
    
    dependencies, code} = item
        graph[item.fileName] = {
    
    
            dependencies,
            code
        }
    }

    return graph
}

const generateCode = entry => {
    
    
    const codeInfo = createDependciesGraph(entry)

    rmdir().then(()=>{
    
    
        fs.mkdir('dist', () => {
    
    
            for(let key in codeInfo){
    
    
                let value = key.split('/')
                value = value[value.length - 1]

                fs.writeFile(`./dist/${
      
      value}`, codeInfo[key]['code'], [], () => {
    
    })
            
            }
        })
    })

}

const rmdir = async () =>  {
    
    
    return new Promise(async (resolve, reject) => {
    
    

        let err = null

        const isDir = fs.existsSync('dist')

        if(isDir){
    
    
            fs.rmdir('dist', {
    
    recursive: true},  error => {
    
    
                if(error){
    
    
                    err = error
                }
            })
        }

        if(err){
    
    
            reject(err)
            return
        }
        
        setTimeout(()=>{
    
    
            resolve()
        }, 1000)
    })
}


generateCode('./src/index.js')


Resumir

En este punto, nuestra JavaScript打包器implementación simple ha terminado. La implementación de este empaquetador simple solo se usa para comprender y comprender el principio del empaquetador principal.

Nuestro empaquetador actual todavía tiene algunos defectos:

  • Los archivos de directorio anidados no se pueden empaquetar.
  • solo puede empaquetar jsarchivos

Supongo que te gusta

Origin blog.csdn.net/qq_44500360/article/details/128251927
Recomendado
Clasificación